mozillazg/rust-pinyin

0.6 性能大幅下降 [Rust 2018 1.34.2 stable]

vayn opened this issue · 9 comments

vayn commented

0.5 版本 README 测试运行时间0.5s
0.6 版本 README 测试运行时间35s

性能退化出现在 e0740d2integer_hasher.rs#L33

换到 nightly 之后性能正常。

看来如果用 stable 没什么好的解法,要么就得把 BuildHasherDefault 注释掉。

经过在 Rust Telegram 群讨论,建议使用 rust-phf 的 hashmap 来提升性能

@vayn 感谢反馈!测试了一下确实性能下降特别严重,麻烦先用 0.5 版本,我先 revert 一下 #27 然后后面发个新版本。 cc @LuoZijun @hanabi1224

vayn commented

好的,已经退回前一版本了

@vayn 不知是否能提供下测试代码?

@LuoZijun 我测试用的代码:

$ cat src/main.rs
extern crate pinyin;

pub fn main() {
    let hans = "**人";
    let mut args = pinyin::Args::new();

    // 默认输出 [["zhong"] ["guo"] ["ren"]]
    println!("{:?}", pinyin::pinyin(hans, &args));

    // 包含声调 [["zh\u{14d}ng"], ["gu\u{f3}"], ["r\u{e9}n"]]
    args.style = pinyin::Style::Tone;
    println!("{:?}", pinyin::pinyin(hans, &args));

    // 声调用数字表示 [["zho1ng"] ["guo2"] ["re2n"]]
    args.style = pinyin::Style::Tone2;
    println!("{:?}", pinyin::pinyin(hans, &args));

    // 开启多音字模式
    args = pinyin::Args::new();
    args.heteronym = true;
    // [["zhong"] ["guo"] ["ren"]]
    println!("{:?}", pinyin::pinyin(hans, &args));

    // [["zho1ng", "zho4ng"] ["guo2"] ["re2n"]]
    args.style = pinyin::Style::Tone2;
    println!("{:?}", pinyin::pinyin(hans, &args));

    // ["zho1ng", "guo2", "re2n"]
    println!("{:?}", pinyin::lazy_pinyin(hans, &args));
}

pinyin = "0.5":

$ time ./target/debug/_t
[["zhong"], ["guo"], ["ren"]]
[["zhōng"], ["guó"], ["rén"]]
[["zho1ng"], ["guo2"], ["re2n"]]
[["zhong", "zhong"], ["guo"], ["ren"]]
[["zho1ng", "zho4ng"], ["guo2"], ["re2n"]]
["zho1ng", "guo2", "re2n"]

real	0m0.011s
user	0m0.002s
sys	0m0.004s

pinyin = "0.6":

$ time ./target/debug/_t
[["zhong"], ["guo"], ["ren"]]
[["zhōng"], ["guó"], ["rén"]]
[["zho1ng"], ["guo2"], ["re2n"]]
[["zhong"], ["guo"], ["ren"]]
[["zho1ng", "zho4ng"], ["guo2"], ["re2n"]]
["zho1ng", "guo2", "re2n"]

real	0m27.724s
user	0m27.353s
sys	0m0.173s

基于 HashMap 的版本之所以出现查询慢的问题是因为 HashMap 的算法是基于运行时的,这意味着在程序首次运行的时候,程序会开始构建 HashMap 的数据结构,这个过程并不是在编译时完成的。
相关代码: e0740d2#diff-406f6e2f8fed8b53cd4b57af84a4a5b5R6

当然第二次以及后面的运行,就不再需要这个构建开销了。这个是一次性开销。

另外,为什么在 Rust 稳定版里面这个一次性的构建开销要远比 Rust 每日构建版 的开销要大呢?

这是因为标准库的 HashMap 的算法不同导致的,每日构建版里面的 HashMap 算法已经更改(正是为了解决这个问题)。至于这两种算法的优缺点这里就不展开了。

参考:

https://doc.rust-lang.org/std/collections/struct.HashMap.html
https://doc.rust-lang.org/nightly/std/collections/struct.HashMap.html
https://github.com/rust-lang/hashbrown

测试代码:

Cargo.toml:

[package]
name = "test_pinyin"
version = "0.1.0"
authors = ["luozijun <luozijun.assistant@gmail.com>"]
edition = "2018"

[dependencies]
pinyin05 = { version = "0.5", package = "pinyin" }
pinyin06 = { version = "0.6", package = "pinyin" }

src/main.rs

extern crate pinyin05;
extern crate pinyin06;

use std::time::Instant;


fn test_version_05(text: &str) {
    println!("Version 05:");

    let mut args = pinyin05::Args::new();
    
    pinyin05::pinyin(text, &args);

    args.style = pinyin05::Style::Tone;
    pinyin05::pinyin(text, &args);

    args.style = pinyin05::Style::Tone2;
    pinyin05::pinyin(text, &args);

    args = pinyin05::Args::new();
    args.heteronym = true;
    pinyin05::pinyin(text, &args);

    args.style = pinyin05::Style::Tone2;
    pinyin05::pinyin(text, &args);

    pinyin05::lazy_pinyin(text, &args);
}

fn test_version_06(text: &str) {
    println!("Version 06:");

    let mut args = pinyin06::Args::new();
    
    pinyin06::pinyin(text, &args);

    args.style = pinyin06::Style::Tone;
    pinyin06::pinyin(text, &args);

    args.style = pinyin06::Style::Tone2;
    pinyin06::pinyin(text, &args);

    args = pinyin06::Args::new();
    args.heteronym = true;
    pinyin06::pinyin(text, &args);

    args.style = pinyin06::Style::Tone2;
    pinyin06::pinyin(text, &args);

    pinyin06::lazy_pinyin(text, &args);
}

fn main() {
    let text = "
重印前记《围城》一九四七年在上海初版,一九四八年再版,一九四九年三版,以后国内没有重印过。
偶然碰见它的新版,那都是香港的“盗印”本。没有看到**的“盗印”,据说在那里它是禁书。
美国哥伦比亚大学夏志清教授的英文著作里对它作了过高的评价,导致了一些西方语言的译本。
日本京都大学荒井健教授很久以前就通知我他要翻译,近年来也陆续在刊物上发表了译文。
现在,人民文学出版社建议重新排印,以便原著在国内较易找着,我感到意外和忻辛。

我写完《围城》,就对它不很满意。出版了我现在更不满意的一本文学批评以后,我抽空又长篇小说,命名《百合心》,
也脱胎于法文成语(Iecoeurd“artichaut),中心人物是一个女角。大约已写成了两万字。
一九四九年夏天,全家从上海迁居北京,手忙脚乱中,我把一叠看来像乱纸的草稿扔到不知哪里去了。
兴致大扫,一直没有再鼓起来,倒也从此省心省事。
年复一年,创作的冲动随年衰减,创作的能力逐渐消失——也许两者根本上是一回事,
我们常把自己的写作冲动误认为自己的写作才能,自以为要写就意味着会写。
相传幸运女神偏向着年轻小伙子,料想文艺女神也不会喜欢老头儿的;不用说有些例外,
而有例外正因为有公例。我慢慢地从省心进而收心,不作再写小说的打算。事隔三十余年,
我也记不清楚当时腹稿里的人物和情节。
就是追忆清楚了,也还算不得数,因为开得出菜单并不等于摆得成酒席,要不然,
谁都可以马上称为善做菜的名厨师又兼大请客的阔东道主了,
秉承曹雪芹遗志而拟定”后四十回“提纲的学者们也就可以凑得成和的得上一个或半个高鹗了。
剩下来的只是一个顽固的信念:假如《百合心》写得成,它会比《围城》好一点。
事情没有做成的人老有这类根据不充分的信念;我们对采摘不到的葡萄,
不但想像它酸,也很可能想像它是分外地甜。
";

    let now = Instant::now();
    test_version_05(text);
    let elapsed = now.elapsed();
    println!("Duration: {} milliseconds or {} microseconds\n",
            elapsed.as_millis(),
            elapsed.as_micros());

    // 注: 06 版本 由于采用了基于运行时的 HashMap
    //     所以第一次运行时会把 Slice 数据复制进 HashMap 中,这个过程需要一些时间。
    //     但是这个复制过程只会在首次运行时发生。
    println!("NOTE: 第一次运行会有初始化的过程,所以速度较慢。");
    let now = Instant::now();
    test_version_06(text);
    let elapsed = now.elapsed();
    println!("Duration: {} milliseconds or {} microseconds\n",
            elapsed.as_millis(),
            elapsed.as_micros());

    println!("NOTE: 第二次运行不再需要初始化,所以速度较快。");
    let now = Instant::now();
    test_version_06(text);
    let elapsed = now.elapsed();
    println!("Duration: {} milliseconds or {} microseconds\n",
            elapsed.as_millis(),
            elapsed.as_micros());
    
    println!("NOTE: 第三次运行不再需要初始化,所以速度较快。");
    let now = Instant::now();
    test_version_06(text);
    let elapsed = now.elapsed();
    println!("Duration: {} milliseconds or {} microseconds\n",
            elapsed.as_millis(),
            elapsed.as_micros());
}

Output:

cargo run

Version 05:
Duration: 58 milliseconds or 58469 microseconds

NOTE: 第一次运行会有初始化的过程,所以速度较慢。
Version 06:
Duration: 1020 milliseconds or 1020688 microseconds

NOTE: 第二次运行不再需要初始化,所以速度较快。
Version 06:
Duration: 93 milliseconds or 93273 microseconds

NOTE: 第三次运行不再需要初始化,所以速度较快。
Version 06:
Duration: 102 milliseconds or 102822 microseconds


cargo run --release

Version 05:
Duration: 8 milliseconds or 8455 microseconds

NOTE: 第一次运行会有初始化的过程,所以速度较慢。
Version 06:
Duration: 27 milliseconds or 27782 microseconds

NOTE: 第二次运行不再需要初始化,所以速度较快。
Version 06:
Duration: 10 milliseconds or 10894 microseconds

NOTE: 第三次运行不再需要初始化,所以速度较快。
Version 06:
Duration: 12 milliseconds or 12141 microseconds

上面提到了原因,那么如果要解决 稳定版的 速度问题,可选项就是在稳定版里面采用 每日构建版的 HashMap 算法,也就是 https://docs.rs/hashbrown

@mozillazg @hanabi1224 不知道你们怎么看?

vayn commented

@LuoZijun 我用来测试的就是 @mozillazg 的代码。现在除非预热并常驻,否则运行时间是无法接受的。不地作为一个工具库,很难限定使用者的运行场景。

@vayn @mozillazg

PR #36 采用了全新的设计,不再有运行时开销。预计会马上会合并,所以这里我先关闭了。