From bf04793165b388c69db541ea38636ea72ed0d8b3 Mon Sep 17 00:00:00 2001 From: Robert Nagy Date: Tue, 27 Apr 2021 11:30:33 +0200 Subject: [PATCH] perf: enable wasm simd (#735) * perf: enable wasm simd * bench: enable simd * build: add wasm simd * ci: use node 16 in benchmarks * test: add simd test script * ci: add simd bench * enable simd by default in tests and benchmarks * fix machine specs in README.md Co-authored-by: Robert Nagy * client: fallback to non-simd on all errors * fixup: re-enable jest * fixup Co-authored-by: Daniele Belardi --- .taprc | 1 + README.md | 38 ++++++++++++++++++++---------------- build/wasm.js | 22 +++++++++++++++++++++ lib/client.js | 26 ++++++++++++++++++------ lib/llhttp/llhttp_simd.wasm | Bin 0 -> 42068 bytes package.json | 3 ++- 6 files changed, 66 insertions(+), 24 deletions(-) create mode 100755 lib/llhttp/llhttp_simd.wasm diff --git a/.taprc b/.taprc index f923754ce1f..102aa5e8162 100644 --- a/.taprc +++ b/.taprc @@ -5,3 +5,4 @@ coverage: true expose-gc: true timeout: 60 check-coverage: false +node-arg: --experimental-wasm-simd diff --git a/README.md b/README.md index 3a69b339579..7f32081461c 100644 --- a/README.md +++ b/README.md @@ -17,32 +17,36 @@ npm i undici ## Benchmarks -AMD EPYC 7502P 32 Core, Node 15 +Node 16 The benchmark is a simple `hello world` [example](benchmarks/index.js) using a number of unix sockets (connections) with a pipelining depth of 10. ### Connections 1 -| Test | Samples | Result | Tolerance | Difference with slowest | -|---------------------|---------|----------------|-----------|-------------------------| -| http - no keepalive | 99 | 812 reqs/sec | ± 0.22 % | | -| http - keepalive | 99 | 819 reqs/sec | ± 0.20 % | + 0.82 % | -| undici - pipeline | 99 | 6632 reqs/sec | ± 0.63 % | + 716.73 % | -| undici - request | 99 | 6645 reqs/sec | ± 1.34 % | + 718.34 % | -| undici - stream | 99 | 7366 reqs/sec | ± 0.59 % | + 807.11 % | -| undici - dispatch | 99 | 7404 reqs/sec | ± 0.37 % | + 811.76 % | +| Tests | Samples | Result | Tolerance | Difference with slowest | +|---------------------|---------|---------------|-----------|-------------------------| +| http - no keepalive | 15 | 4.63 req/sec | ± 2.77 % | - | +| http - keepalive | 10 | 4.81 req/sec | ± 2.16 % | + 3.94 % | +| undici - stream | 25 | 62.22 req/sec | ± 2.67 % | + 1244.58 % | +| undici - dispatch | 15 | 64.33 req/sec | ± 2.47 % | + 1290.24 % | +| undici - request | 15 | 66.08 req/sec | ± 2.48 % | + 1327.88 % | +| undici - pipeline | 10 | 66.13 req/sec | ± 1.39 % | + 1329.08 % | ### Connections 50 -| Test | Samples | Result | Tolerance | Difference with slowest | -|---------------------|---------|----------------|-----------|-------------------------| -| http - no keepalive | 99 | 12968 reqs/sec | ± 1.86 % | | -| http - keepalive | 99 | 14745 reqs/sec | ± 1.59 % | + 13.70 % | -| undici - pipeline | 99 | 20051 reqs/sec | ± 2.34 % | + 54.62 % | -| undici - stream | 100 | 26456 reqs/sec | ± 3.50 % | + 104.00 % | -| undici - request | 99 | 29342 reqs/sec | ± 1.26 % | + 126.26 % | -| undici - dispatch | 99 | 35323 reqs/sec | ± 0.77 % | + 172.38 % | +| Tests | Samples | Result | Tolerance | Difference with slowest | +|---------------------|---------|------------------|-----------|-------------------------| +| http - no keepalive | 50 | 3546.49 req/sec | ± 2.90 % | - | +| http - keepalive | 15 | 5692.67 req/sec | ± 2.48 % | + 60.52 % | +| undici - pipeline | 25 | 8478.71 req/sec | ± 2.62 % | + 139.07 % | +| undici - request | 20 | 9766.66 req/sec | ± 2.79 % | + 175.39 % | +| undici - stream | 15 | 10109.74 req/sec | ± 2.94 % | + 185.06 % | +| undici - dispatch | 25 | 10949.73 req/sec | ± 2.54 % | + 208.75 % | + +#### Note + +The benchmarks have the [simd](https://github.com/WebAssembly/simd) feature enabled. ## Quick Start diff --git a/build/wasm.js b/build/wasm.js index ca16e5caabe..7c9c920c9c4 100644 --- a/build/wasm.js +++ b/build/wasm.js @@ -42,3 +42,25 @@ execSync(`${WASI_ROOT}/bin/clang \ ${join(WASM_SRC, 'src')}/*.c \ -I${join(WASM_SRC, 'include')} \ -o ${join(WASM_OUT, 'llhttp.wasm')}`, { stdio: 'inherit' }) + +// Build wasm simd binary +execSync(`${WASI_ROOT}/bin/clang \ + --sysroot=${WASI_ROOT}/share/wasi-sysroot \ + -target wasm32-unknown-wasi \ + -msimd128 \ + -Ofast \ + -fno-exceptions \ + -fvisibility=hidden \ + -mexec-model=reactor \ + -Wl,-error-limit=0 \ + -Wl,-O3 \ + -Wl,--lto-O3 \ + -Wl,--strip-all \ + -Wl,--allow-undefined \ + -Wl,--export-dynamic \ + -Wl,--export-table \ + -Wl,--export=malloc \ + -Wl,--export=free \ + ${join(WASM_SRC, 'src')}/*.c \ + -I${join(WASM_SRC, 'include')} \ + -o ${join(WASM_OUT, 'llhttp_simd.wasm')}`, { stdio: 'inherit' }) diff --git a/lib/client.js b/lib/client.js index ed846c2417f..937232ad3f5 100644 --- a/lib/client.js +++ b/lib/client.js @@ -23,12 +23,6 @@ const { BodyTimeoutError } = require('./core/errors') -const { resolve } = require('path') -const { readFileSync } = require('fs') -const constants = require('./llhttp/constants') -const EMPTY_BUF = Buffer.alloc(0) -const mod = new WebAssembly.Module(readFileSync(resolve(__dirname, './llhttp/llhttp.wasm'))) - const { kUrl, kReset, @@ -387,6 +381,26 @@ class HTTPParserError extends Error { } } +let mod, build +const { resolve } = require('path') +const { readFileSync } = require('fs') +const constants = require('./llhttp/constants') +const EMPTY_BUF = Buffer.alloc(0) + +try { + build = resolve(__dirname, './llhttp/llhttp_simd.wasm') + const bin = readFileSync(build) + mod = new WebAssembly.Module(bin) +} catch (e) { + // We could check if the error was caused by the simd option not + // being enabled, but the occurring of this other error + // * https://github.com/emscripten-core/emscripten/issues/11495 + // got me to remove that check to avoid breaking Node 12. + build = resolve(__dirname, './llhttp/llhttp.wasm') + const bin = readFileSync(build) + mod = new WebAssembly.Module(bin) +} + const llhttp = new WebAssembly.Instance(mod, { env: { /* eslint-disable camelcase */ diff --git a/lib/llhttp/llhttp_simd.wasm b/lib/llhttp/llhttp_simd.wasm new file mode 100755 index 0000000000000000000000000000000000000000..52db42fb67d4128e68434620c550f7484b0a0a76 GIT binary patch literal 42068 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 d-AhOJ^{)(*_!Ac8sPbx5wn|GQ_bY1Ze*$S7*v