From 23f7307bb53773d22726e47b31f6888074886446 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Wed, 24 May 2023 13:32:34 +0800 Subject: [PATCH] perf: improve performance by reduce memory allocation --- Cargo.toml | 3 +- README.md | 45 ++++++++++++----- benchmark/bench.ts | 16 ++++-- index.d.ts | 4 +- package.json | 3 +- src/lib.rs | 123 +++++++++++++++++++++++++++++---------------- yarn.lock | 8 +++ 7 files changed, 139 insertions(+), 63 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 96b67bd..2f75d7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] authors = ["LongYinan "] -edition = "2018" +edition = "2021" name = "napi-pinyin" version = "0.1.0" @@ -27,3 +27,4 @@ napi-build = "2" [profile.release] lto = true opt-level = 3 +codegen-units = 1 \ No newline at end of file diff --git a/README.md b/README.md index 98d75b2..888ec00 100644 --- a/README.md +++ b/README.md @@ -45,41 +45,63 @@ yarn add @napi-rs/pinyin ## 与 [pinyin](https://github.com/hotoo/pinyin) 性能对比 -Benchmark over `pinyin` package: +Benchmark over [`pinyin`](https://github.com/hotoo/pinyin) and [`pinyin-pro`](https://github.com/zh-lx/pinyin-pro) package: + +> **Note** +> +> [`pinyin-pro`](https://github.com/zh-lx/pinyin-pro) doesn't support segment feature. + +System info + +``` +OS: macOS 12.3.1 21E258 arm64 +Host: MacBookPro18,2 +Kernel: 21.4.0 +Shell: zsh 5.8 +CPU: Apple M1 Max +GPU: Apple M1 Max +Memory: 9539MiB / 65536MiB +``` ```bash Running "Short input without segment" suite... Progress: 100% @napi-rs/pinyin: - 962 035 ops/s, ±0.68% | fastest + 2 168 325 ops/s, ±0.36% | fastest + + pinyin-pro: + 1 595 118 ops/s, ±0.18% | 26.44% slower node-pinyin: - 434 241 ops/s, ±0.66% | slowest, 54.86% slower + 522 675 ops/s, ±0.09% | slowest, 75.89% slower -Finished 2 cases! +Finished 3 cases! Fastest: @napi-rs/pinyin Slowest: node-pinyin Running "Long input without segment" suite... Progress: 100% @napi-rs/pinyin: - 59 ops/s, ±0.83% | fastest + 566 ops/s, ±0.27% | fastest + + pinyin-pro: + 215 ops/s, ±0.60% | 62.01% slower node-pinyin: - 2 ops/s, ±3.30% | slowest, 96.61% slower + 1 ops/s, ±3.09% | slowest, 99.82% slower -Finished 2 cases! +Finished 3 cases! Fastest: @napi-rs/pinyin Slowest: node-pinyin Running "Short input with segment" suite... Progress: 100% @napi-rs/pinyin: - 530 228 ops/s, ±1.94% | fastest + 885 238 ops/s, ±2.00% | fastest node-pinyin: - 307 788 ops/s, ±0.83% | slowest, 41.95% slower + 475 546 ops/s, ±0.42% | slowest, 46.28% slower Finished 2 cases! Fastest: @napi-rs/pinyin @@ -88,15 +110,14 @@ Running "Long input with segment" suite... Progress: 100% @napi-rs/pinyin: - 152 ops/s, ±1.09% | fastest + 317 ops/s, ±0.46% | fastest node-pinyin: - 3 ops/s, ±3.08% | slowest, 98.03% slower + 5 ops/s, ±3.66% | slowest, 98.42% slower Finished 2 cases! Fastest: @napi-rs/pinyin Slowest: node-pinyin -✨ Done in 53.36s. ``` ## 用法 diff --git a/benchmark/bench.ts b/benchmark/bench.ts index 18c3f19..8656174 100644 --- a/benchmark/bench.ts +++ b/benchmark/bench.ts @@ -4,11 +4,13 @@ import { join } from 'path' import b from 'benny' import { Summary } from 'benny/lib/internal/common-types' import nodePinyin from 'pinyin' +import { pinyin as pinyinPro } from 'pinyin-pro' import { pinyin } from '../index' const short = '你好拼音' -const long = readFileSync(join(__dirname, 'long.txt'), 'utf-8') +const long = readFileSync(join(__dirname, 'long.txt')) +const longText = long.toString('utf8') async function run() { const output = [ @@ -19,6 +21,10 @@ async function run() { pinyin(short) }), + b.add('pinyin-pro', () => { + pinyinPro(short) + }), + b.add('node-pinyin', () => { nodePinyin(short) }), @@ -34,8 +40,12 @@ async function run() { pinyin(long) }), + b.add('pinyin-pro', () => { + pinyinPro(longText) + }), + b.add('node-pinyin', () => { - nodePinyin(long) + nodePinyin(longText) }), b.cycle(), @@ -64,7 +74,7 @@ async function run() { }), b.add('node-pinyin', () => { - nodePinyin(long, { segment: true }) + nodePinyin(longText, { segment: true }) }), b.cycle(), diff --git a/index.d.ts b/index.d.ts index ee46990..2a0fe73 100644 --- a/index.d.ts +++ b/index.d.ts @@ -21,6 +21,6 @@ export interface PinyinConvertOptions { heteronym?: boolean segment?: boolean } -export function pinyin(inputStr: string, opt?: PinyinConvertOptions | undefined | null): string[] | string[][] -export function asyncPinyin(input: string, opt?: PinyinConvertOptions | undefined | null, signal?: AbortSignal | undefined | null): Promise +export function pinyin(input: string | Buffer, opt?: PinyinConvertOptions | undefined | null): string[] | string[][] +export function asyncPinyin(input: string | Buffer, opt?: PinyinConvertOptions | undefined | null, signal?: AbortSignal | undefined | null): Promise export function compare(inputA: string, inputB: string): number diff --git a/package.json b/package.json index 46c24c3..164ad1d 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ }, "scripts": { "artifacts": "napi artifacts", - "bench": "node -r ts-node/register/transpile-only benchmark/bench.ts", + "bench": "node -r @swc-node/register benchmark/bench.ts", "build": "napi build --platform --release --pipe \"prettier -w\"", "build:debug": "napi build --platform --pipe \"prettier -w\"", "format": "run-p format:md format:json format:yaml format:source format:rs", @@ -92,6 +92,7 @@ "npm-run-all": "^4.1.5", "pinst": "^3.0.0", "pinyin": "^2.11.2", + "pinyin-pro": "^3.14.0", "prettier": "^2.8.1", "typescript": "^5.0.0" }, diff --git a/src/lib.rs b/src/lib.rs index 10e6eff..c118c70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,8 +6,8 @@ extern crate napi_derive; use std::convert::TryFrom; use jieba_rs::Jieba; -use napi::bindgen_prelude::*; -use once_cell::sync::OnceCell; +use napi::{bindgen_prelude::*, JsBuffer, NapiRaw, NapiValue}; +use once_cell::sync::Lazy; use pinyin::{Pinyin, ToPinyin, ToPinyinMulti}; use rayon::prelude::*; @@ -15,7 +15,7 @@ use rayon::prelude::*; #[global_allocator] static ALLOC: mimalloc_rust::GlobalMiMalloc = mimalloc_rust::GlobalMiMalloc; -static JIEBA: OnceCell = OnceCell::new(); +static JIEBA: Lazy = Lazy::new(Jieba::new); #[napi(js_name = "PINYIN_STYLE")] #[derive(Debug)] @@ -53,7 +53,7 @@ impl TryFrom for PinyinStyle { pub struct AsyncPinyinTask { style: PinyinStyle, - input: String, + input: Either, option: PinyinOption, } @@ -95,13 +95,13 @@ impl Task for AsyncPinyinTask { type JsValue = napi::JsObject; fn compute(&mut self) -> Result { + let input = get_chars_buffer(&self.input); match self.option { PinyinOption::Default => { - let input_chars = self.input.chars(); - let input_len = self.input.len(); + let input_len = input.len(); let mut output_py: Vec = Vec::with_capacity(input_len); let mut non_hans_chars: Vec = Vec::with_capacity(input_len); - for c in input_chars { + for c in input.chars() { if let Some(py) = c.to_pinyin() { if !non_hans_chars.is_empty() { output_py.push(non_hans_chars.par_iter().collect::()); @@ -118,9 +118,8 @@ impl Task for AsyncPinyinTask { Ok(PinyinData::Default(output_py)) } PinyinOption::SegmentDefault => { - let jieba = JIEBA.get_or_init(Jieba::new); - let input_words = jieba.cut_all(self.input.as_str()); - let input_len = self.input.len(); + let input_words = JIEBA.cut_all(input); + let input_len = input.len(); let mut output_py: Vec = Vec::with_capacity(input_len); let mut has_pinyin = false; let mut non_hans = String::with_capacity(input_len); @@ -143,20 +142,25 @@ impl Task for AsyncPinyinTask { Ok(PinyinData::Default(output_py)) } PinyinOption::Multi => { - let input_chars = self.input.chars(); - let input_len = self.input.len(); + let input_chars = input.chars(); + let input_len = input.len(); let mut output_multi_py: Vec> = Vec::with_capacity(input_len); let mut non_hans_chars: Vec = Vec::with_capacity(input_len); for c in input_chars { if let Some(multi_py) = c.to_pinyin_multi() { if !non_hans_chars.is_empty() { - output_multi_py.push(vec![non_hans_chars.par_iter().collect::()]); + output_multi_py.push(vec![if non_hans_chars.len() >= 1024 { + non_hans_chars.par_iter().collect::() + } else { + non_hans_chars.iter().collect::() + }]); non_hans_chars.clear(); } - let mut multi_py_vec = Vec::with_capacity(multi_py.count()); - for py in multi_py { - multi_py_vec.push(get_pinyin(py, self.style).to_owned()); - } + let multi_py_vec = multi_py + .into_iter() + .map(|py| get_pinyin(py, self.style).to_owned()) + .collect(); + output_multi_py.push(multi_py_vec); } else { non_hans_chars.push(c); @@ -168,11 +172,10 @@ impl Task for AsyncPinyinTask { Ok(PinyinData::Multi(output_multi_py)) } PinyinOption::SegmentMulti => { - let jieba = JIEBA.get_or_init(Jieba::new); - let input_words = jieba.cut_all(self.input.as_str()); - let input_len = self.input.len(); + let input_words = JIEBA.cut_all(input); + let input_len = input.len(); let mut output_multi_py: Vec> = Vec::with_capacity(input_len); - let mut non_hans = String::with_capacity(self.input.len()); + let mut non_hans = String::with_capacity(input.len()); for word in input_words { let multi_py = word.to_pinyin_multi(); let mut has_pinyin = false; @@ -181,10 +184,10 @@ impl Task for AsyncPinyinTask { output_multi_py.push(vec![non_hans.clone()]); non_hans.clear(); } - let mut multi_py_vec = Vec::with_capacity(py.count()); - for p in py { - multi_py_vec.push(get_pinyin(p, self.style).to_owned()); - } + let multi_py_vec = py + .into_iter() + .map(|p| get_pinyin(p, self.style).to_owned()) + .collect(); output_multi_py.push(multi_py_vec); has_pinyin = true; } @@ -228,7 +231,11 @@ impl Task for AsyncPinyinTask { } #[napi(js_name = "pinyin", ts_return_type = "string[] | string[][]")] -pub fn to_pinyin(env: Env, input_str: String, opt: Option) -> Result { +pub fn to_pinyin( + env: Env, + input: Either, + opt: Option, +) -> Result { let opt = opt.unwrap_or(PinyinConvertOptions { style: Some(PinyinStyle::Plain), segment: Some(false), @@ -236,7 +243,7 @@ pub fn to_pinyin(env: Env, input_str: String, opt: Option) }); let option = to_option(opt.segment.unwrap_or(false), opt.heteronym.unwrap_or(false)); let style = opt.style.unwrap_or(PinyinStyle::Plain); - + let input_str = get_chars(env, &input)?; match option { PinyinOption::Default => { let mut result_arr = Vec::new(); @@ -245,7 +252,11 @@ pub fn to_pinyin(env: Env, input_str: String, opt: Option) for c in input_chars { if let Some(py) = c.to_pinyin() { if !non_hans_chars.is_empty() { - result_arr.push(non_hans_chars.par_iter().collect::()); + result_arr.push(if non_hans_chars.len() >= 1024 { + non_hans_chars.par_iter().collect::() + } else { + non_hans_chars.iter().collect::() + }); non_hans_chars.clear(); } result_arr.push(get_pinyin(py, style).to_owned()); @@ -254,14 +265,17 @@ pub fn to_pinyin(env: Env, input_str: String, opt: Option) } } if !non_hans_chars.is_empty() { - result_arr.push(non_hans_chars.into_par_iter().collect::()); + result_arr.push(if non_hans_chars.len() >= 1024 { + non_hans_chars.into_par_iter().collect::() + } else { + non_hans_chars.into_iter().collect::() + }); } Array::from_vec(&env, result_arr) } PinyinOption::SegmentDefault => { let mut result_arr = Vec::new(); - let jieba = JIEBA.get_or_init(Jieba::new); - let input_words = jieba.cut(input_str.as_str(), false); + let input_words = JIEBA.cut(input_str, false); let mut non_hans = String::with_capacity(input_str.len()); for word in input_words { let mut has_pinyin = false; @@ -293,10 +307,10 @@ pub fn to_pinyin(env: Env, input_str: String, opt: Option) result_arr.push(buf_arr); non_hans_chars.clear(); } - let mut multi_py_vec = Vec::with_capacity(multi_py.count()); - for py in multi_py.into_iter() { - multi_py_vec.push(get_pinyin(py, style).to_owned()); - } + let multi_py_vec = multi_py + .into_iter() + .map(|py| get_pinyin(py, style).to_owned()) + .collect(); result_arr.push(multi_py_vec); } else { non_hans_chars.push(c); @@ -310,8 +324,7 @@ pub fn to_pinyin(env: Env, input_str: String, opt: Option) } PinyinOption::SegmentMulti => { let mut result_arr = Vec::new(); - let jieba = JIEBA.get_or_init(Jieba::new); - let input_words = jieba.cut(input_str.as_str(), false); + let input_words = JIEBA.cut(input_str, false); let mut non_hans = String::with_capacity(input_str.len()); for word in input_words { let multi_py = word.to_pinyin_multi(); @@ -322,10 +335,11 @@ pub fn to_pinyin(env: Env, input_str: String, opt: Option) result_arr.push(buf_arr); non_hans.clear(); } - let mut multi_py_vec = Vec::with_capacity(py.count()); - for p in py.into_iter() { - multi_py_vec.push(get_pinyin(p, style).to_owned()); - } + let multi_py_vec = py + .into_iter() + .map(|p| get_pinyin(p, style).to_owned()) + .collect(); + result_arr.push(multi_py_vec); has_pinyin = true; } @@ -342,7 +356,6 @@ pub fn to_pinyin(env: Env, input_str: String, opt: Option) } } -#[inline(always)] fn get_pinyin(input: Pinyin, style: PinyinStyle) -> &'static str { match style { PinyinStyle::Plain => input.plain(), @@ -353,9 +366,32 @@ fn get_pinyin(input: Pinyin, style: PinyinStyle) -> &'static str { } } +fn get_chars(env: Env, input: &Either) -> Result<&str> { + match input { + Either::A(input) => Ok(input.as_str()), + Either::B(input) => { + let buf = unsafe { JsBuffer::from_raw_unchecked(env.raw(), input.raw()) }; + let buf_value = buf.into_value()?; + Ok(unsafe { + std::str::from_utf8_unchecked(std::slice::from_raw_parts( + buf_value.as_ptr(), + buf_value.len(), + )) + }) + } + } +} + +fn get_chars_buffer(input: &Either) -> &str { + match input { + Either::A(input) => input.as_str(), + Either::B(input) => unsafe { std::str::from_utf8_unchecked(input.as_ref()) }, + } +} + #[napi(ts_return_type = "Promise")] pub fn async_pinyin( - input: String, + input: Either, opt: Option, signal: Option, ) -> Result> { @@ -374,7 +410,6 @@ pub fn async_pinyin( Ok(AsyncTask::with_optional_signal(task, signal)) } -#[inline(always)] fn to_option(need_segment: bool, should_to_multi: bool) -> PinyinOption { (u8::from(need_segment) << 1 | u8::from(should_to_multi)).into() } diff --git a/yarn.lock b/yarn.lock index 0665828..4498e3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -186,6 +186,7 @@ __metadata: npm-run-all: ^4.1.5 pinst: ^3.0.0 pinyin: ^2.11.2 + pinyin-pro: ^3.14.0 prettier: ^2.8.1 typescript: ^5.0.0 languageName: unknown @@ -4324,6 +4325,13 @@ __metadata: languageName: node linkType: hard +"pinyin-pro@npm:^3.14.0": + version: 3.14.0 + resolution: "pinyin-pro@npm:3.14.0" + checksum: fdb159d0e3461d22c12421d1454d2a88e13ca085561ab3ff3cfae713ca9e66eb1de676dfe1d7eb9b2e86d3d7d981b806f186e1f2fa4b7843fc075f7a611ceb68 + languageName: node + linkType: hard + "pinyin@npm:^2.11.2": version: 2.11.2 resolution: "pinyin@npm:2.11.2"