node icon indicating copy to clipboard operation
node copied to clipboard

src: improve TextEncoder encodeInto performance

Open anonrig opened this issue 8 months ago • 5 comments

Improves the performance of TextEncoder.encodeInto method

Benchmark CI: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1712/

It seems none of the benchmarks are triggering the fast path

                                                                               confidence improvement accuracy (*)   (**)  (***)
util/text-encoder.js op='encode' type='ascii' n=1000000 len=1024                              -0.77 %       ±1.08% ±1.44% ±1.87%
util/text-encoder.js op='encode' type='ascii' n=1000000 len=256                                0.70 %       ±0.97% ±1.29% ±1.67%
util/text-encoder.js op='encode' type='ascii' n=1000000 len=32                                -0.34 %       ±1.12% ±1.50% ±1.95%
util/text-encoder.js op='encode' type='ascii' n=1000000 len=8192                               0.08 %       ±0.09% ±0.12% ±0.15%
util/text-encoder.js op='encode' type='one-byte-string' n=1000000 len=1024                     0.36 %       ±0.61% ±0.81% ±1.05%
util/text-encoder.js op='encode' type='one-byte-string' n=1000000 len=256                      0.29 %       ±0.64% ±0.85% ±1.10%
util/text-encoder.js op='encode' type='one-byte-string' n=1000000 len=32                       0.48 %       ±0.95% ±1.27% ±1.65%
util/text-encoder.js op='encode' type='one-byte-string' n=1000000 len=8192                    -0.02 %       ±0.05% ±0.06% ±0.08%
util/text-encoder.js op='encode' type='two-byte-string' n=1000000 len=1024             **      0.26 %       ±0.15% ±0.20% ±0.26%
util/text-encoder.js op='encode' type='two-byte-string' n=1000000 len=256              **     -0.73 %       ±0.45% ±0.60% ±0.79%
util/text-encoder.js op='encode' type='two-byte-string' n=1000000 len=32                       0.02 %       ±0.84% ±1.11% ±1.45%
util/text-encoder.js op='encode' type='two-byte-string' n=1000000 len=8192            ***     -0.15 %       ±0.02% ±0.03% ±0.04%
util/text-encoder.js op='encodeInto' type='ascii' n=1000000 len=1024                  ***     -0.64 %       ±0.21% ±0.29% ±0.37%
util/text-encoder.js op='encodeInto' type='ascii' n=1000000 len=256                   ***     -2.94 %       ±0.56% ±0.75% ±0.99%
util/text-encoder.js op='encodeInto' type='ascii' n=1000000 len=32                    ***     -8.50 %       ±2.68% ±3.57% ±4.64%
util/text-encoder.js op='encodeInto' type='ascii' n=1000000 len=8192                  ***     -0.05 %       ±0.02% ±0.02% ±0.03%
util/text-encoder.js op='encodeInto' type='one-byte-string' n=1000000 len=1024        ***     -0.99 %       ±0.22% ±0.29% ±0.38%
util/text-encoder.js op='encodeInto' type='one-byte-string' n=1000000 len=256         ***     -1.47 %       ±0.81% ±1.08% ±1.41%
util/text-encoder.js op='encodeInto' type='one-byte-string' n=1000000 len=32            *     -2.82 %       ±2.60% ±3.46% ±4.50%
util/text-encoder.js op='encodeInto' type='one-byte-string' n=1000000 len=8192          *     -0.07 %       ±0.06% ±0.08% ±0.10%
util/text-encoder.js op='encodeInto' type='two-byte-string' n=1000000 len=1024        ***     -0.67 %       ±0.13% ±0.18% ±0.23%
util/text-encoder.js op='encodeInto' type='two-byte-string' n=1000000 len=256         ***     -1.69 %       ±0.42% ±0.56% ±0.73%
util/text-encoder.js op='encodeInto' type='two-byte-string' n=1000000 len=32           **     -2.70 %       ±1.93% ±2.57% ±3.35%
util/text-encoder.js op='encodeInto' type='two-byte-string' n=1000000 len=8192        ***      0.11 %       ±0.05% ±0.06% ±0.08%

anonrig avatar Apr 29 '25 17:04 anonrig

Codecov Report

Attention: Patch coverage is 17.39130% with 19 lines in your changes missing coverage. Please review.

Project coverage is 90.16%. Comparing base (723d7bb) to head (06de6fa). Report is 449 commits behind head on main.

Files with missing lines Patch % Lines
src/encoding_binding.cc 17.39% 19 Missing :warning:
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #58080      +/-   ##
==========================================
- Coverage   90.17%   90.16%   -0.02%     
==========================================
  Files         630      630              
  Lines      186473   186495      +22     
  Branches    36613    36615       +2     
==========================================
- Hits       168160   168157       -3     
- Misses      11128    11139      +11     
- Partials     7185     7199      +14     
Files with missing lines Coverage Δ
src/encoding_binding.h 100.00% <ø> (ø)
src/node_external_reference.h 100.00% <ø> (ø)
src/encoding_binding.cc 72.36% <17.39%> (-7.30%) :arrow_down:

... and 24 files with indirect coverage changes

:rocket: New features to boost your workflow:
  • :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • :package: JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

codecov[bot] avatar Apr 29 '25 18:04 codecov[bot]

@anonrig even if the benchmark is not hitting the fast path: the benchmarks currently show a regression. I am surprised that is the case. Is there a possibility to prevent that for non-optimized code?

BridgeAR avatar Apr 30 '25 17:04 BridgeAR

@anonrig even if the benchmark is not hitting the fast path: the benchmarks currently show a regression. I am surprised that is the case. Is there a possibility to prevent that for non-optimized code?

I think that's due to unreliable benchmarks, I'll re-run the benchmarks to see if they persist.

Edit 2: New benchmarks: https://ci.nodejs.org/view/Node.js%20benchmark/job/benchmark-node-micro-benchmarks/1712/

anonrig avatar May 03 '25 14:05 anonrig

It seems it still degrades performance. Opened an issue on v8-dev https://groups.google.com/g/v8-dev/c/x2Fs6do0jDA

anonrig avatar May 03 '25 20:05 anonrig

cc @erikcorry

anonrig avatar May 03 '25 20:05 anonrig

It makes sense to me that the "fast API" is slower in this benchmark than regular API calls. "fast API" is mostly a marketing name, a more telling name would be something like "unboxed API", because primitive values get passed unboxed, i.e. not boxed in a HeapNumber. If there are no parameters of primitive type, there is no reason why the "fast API" should be faster.

If I understood the benchmark correctly, then you even open a HandleScope in the API function. In the case of regular API calls, the HandleScope already gets opened automatically by the API call, which is faster.

Therefore, there is no reason why the fast API would be faster in this example, and there is a reason why regular API calls would be faster, so in total the result is not surprising.

gahaas avatar May 05 '25 05:05 gahaas

If there are no parameters of primitive type, there is no reason why the "fast API" should be faster.

I wonder: what is the benefit of adding Local<Value> as a parameter? Only for DX?

In the case of regular API calls, the HandleScope already gets opened automatically by the API call, which is faster.

@gahaas Where is it getting called automatically? Would you mind sharing the code?

anonrig avatar May 05 '25 15:05 anonrig

If there are no parameters of primitive type, there is no reason why the "fast API" should be faster.

I wonder: what is the benefit of adding Local as a parameter? Only for DX?

There are API functions that have parameters of primitive type and reference type. i.e. web gpu APIs that take a TypedArray and Number parameters. Without supporting Local as a parameter it would not be possible to use the fast API there.

In the case of regular API calls, the HandleScope already gets opened automatically by the API call, which is faster.

@gahaas Where is it getting called automatically? Would you mind sharing the code?

Opening the HandleScope starts around here in the macro assembler.

gahaas avatar May 06 '25 04:05 gahaas