Skip to content

Производительность ​

Горячие участки (модульная арифметика больших чисел, компрессия хеша, раунды блочного шифра, билинейные спаривания) выполняются внутри WASM. Таблица покрывает каждую публичную операцию; столбец gmkitx — эталон на чистом JS на той же машине, с теми же параметрами и сообщениями (операций, которых там нет, помечены —). Измерено на AMD Ryzen 7 7800X3D / Node 24.15 / Windows 11; воспроизводится через pnpm bench.

АлгоритмОперацияsm-crypto-wasmЧистый JS (gmkitx)Отношение
SM3hash 32 B / 1 КБ / 64 КБ872 ns / 4.75 µs / 245 µs14.7 µs (1 КБ)в 3.1× быстрее
SM3hmac 1 КБ5.58 µs19.4 µsв 3.5× быстрее
SM4ECB 16 B / 1 КБ0.94 µs / 6.98 µs37.3 µs (1 КБ)в 5.3× быстрее
SM4CBC 1 КБ10.5 µs41.6 µsв 4.0× быстрее
SM4CTR 1 КБ7.43 µs36.5 µsв 4.9× быстрее
SM4CFB 1 КБ10.0 µs37.9 µsв 3.8× быстрее
SM4OFB 1 КБ9.98 µs36.3 µsв 3.6× быстрее
SM4GCM 1 КБ19.8 µs221 µsв 11.2× быстрее
SM2генерация пары ключей151 µs——
SM2вывод открытого ключа110 µs197 µsв 1.8× быстрее
SM2подпись 64 B142 µs446 µsв 3.1× быстрее
SM2проверка 64 B402 µs2.85 msв 7.1× быстрее
SM2шифрование 64 B499 µs2.96 msв 5.9× быстрее
SM2расшифрование 64 B343 µs2.59 msв 7.6× быстрее
SM2KEM инкапсуляция / декапсуляция (16 B key)496 / 328 µs——
SM2hybrid шифрование / расшифрование 1 MB37.3 / 36.4 ms——
SM2обмен ключами (полный A↔B)1.52 ms11.4 msв 7.5× быстрее
SM9главная пара ключей (подпись / шифр)2.07 / 1.73 ms——
SM9главный открытый ключ (подпись / шифр)97.1 / 49.1 µs——
SM9вывод открытого ключа пользователя563 µs——
SM9подпись 32 B4.11 ms——
SM9проверка 32 B8.00 ms——
SM9шифрование 32 B3.69 ms——
SM9расшифрование 32 B5.40 ms——
SM9KEM инкапсуляция / декапсуляция3.42 / 4.92 ms——
SM9hybrid шифрование / расшифрование 10 KB3.21 / 4.77 ms——
SM9обмен ключами (полный A↔B)19.6 ms——
ZUCEEA3 шифрование 1 КБ9.07 µs20.5 µsв 2.3× быстрее
ZUCEEA3 гамма 1 КБ6.15 µs——
ZUCEIA3 MAC 1 КБ11.2 µs——

Строка SM2 колеблется

SM2 находится за фасадом на TS (подпись использует кеш вывода открытого ключа, шифрование по-прежнему делает hex- и точечные кодеки плюс вызов CSPRNG хоста). Разброс между запусками достигает ±20%, намного больше, чем у SM3 / SM4 / SM9 / ZUC. Вся строка SM2 в таблице взята из одного запуска.

SM2 с фиксированным открытым ключом: ещё 1.9×

После await sm2.preparePublicKey(pub) строится comb-таблица для этого ключа, и encrypt переключает k·P с wNAF (256 удвоений) на comb (52 удвоения): шифрование 64 B — 486 → 260 µs (mitata, эта машина). Построение таблицы ≈ 280 µs, окупается после ≈ 1.2 шифрований. Кеш по умолчанию имеет 8 слотов с LRU; ротация 8 ключей даёт те же цифры, что и одно попадание. Таблица строится только явным prepare, при промахе — откат к wNAF без замедления. kemEncrypt / hybridEncrypt используют тот же путь. Bench: encrypt 64B (prepared pk), encrypt 64B (8 prepared keys, rotating).

Очное сравнение с двумя другими библиотеками ​

Выше эталон — чистый JS gmkitx. Если взять две другие реальные библиотеки китайской криптографии, с теми же параметрами, сообщениями и ключами:

  • Cubelrti/sm-crypto-wasm — Rust + wasm-bindgen, скомпилированный в WebAssembly (⚠️ имя пакета совпадает с этим проектом, но код другой);
  • sm-crypto-v2 — TypeScript + noble-curves, один из самых быстрых вариантов на чистом JS.

Каждая сторона — это release-сборка из своих исходников; в бенчмарк встроена самопроверка, побайтово сравнивающая выводы всех трёх (SM3 / SM4 обязаны совпадать, перекрёстная проверка подписи SM2 обязана проходить) — при расхождении выбрасывается ошибка, а не «красивое» число.

⚠️ В столбце Cubelrti два числа. Его Cargo.toml фиксирует release-профиль на размере (opt-level = "z" + wasm-opt = ["-Oz"]), и предсобранный артефакт, закоммиченный в его репозиторий, — ровно такая сборка (175 КБ против 177 КБ, которые получаю я с той же конфигурацией). Если переключить на скорость (opt-level = 3 + wasm-opt = ["-O3"], 263 КБ), он становится быстрее на 26%~53% (53% на SM3). Поэтому даны оба варианта: конфигурация по умолчанию — это «что вы реально получаете», -O3 — «потолок этой кодовой базы на Rust». Столбцы отношений читаются как во сколько раз этот проект быстрее.

Операцияsm-crypto-wasmCubelrti
по умолч. (-Oz)
Cubelrti
-O3
sm-crypto-v2против Cubelrti
(-Oz)
против Cubelrti
(-O3)
против v2
SM2 подпись 64 B143.6 µs4.51 ms3.03 ms297 µs31.4×21.1×2.07×
SM2 проверка 64 B406.6 µs4.49 ms3.05 ms2.83 ms11.0×7.50×6.96×
SM2 шифрование 64 B506.5 µs4.43 ms2.95 ms2.67 ms8.75×5.82×5.27×
SM2 расшифрование 64 B337.0 µs2.26 ms1.51 ms2.49 ms6.71×4.48×7.39×
SM2 пара ключей147.8 µs2.25 ms1.54 ms210 µs15.2×10.4×1.42×
SM3 hash 1 КБ4.75 µs24.7 µs11.5 µs6.44 µs5.19×2.42×1.36×
SM3 hmac 1 КБ5.61 µs29.3 µs13.7 µs9.27 µs5.22×2.44×1.65×
SM4 ECB шифрование 1 КБ6.78 µs12.6 µs8.83 µs12.6 µs1.85×1.30×1.85×
SM4 CBC шифрование 1 КБ10.4 µs19.5 µs12.5 µs14.9 µs1.88×1.21×1.44×
SM4 CBC расшифрование 1 КБ10.8 µs22.2 µs16.3 µs14.0 µs2.06×1.51×1.30×
SM4 CTR шифрование 1 КБ7.09 µs12.6 µs10.4 µs— (не реализовано)1.77×1.46×—
SM4 GCM шифрование 1 КБ19.9 µs27.5 µs19.7 µs30.4 µs1.39×0.99×1.53×

Обратите внимание на последнюю строку: с -O3 в режиме SM4-GCM Cubelrti практически сравнивается с этим проектом. По операциям все режимы SM4 с -O3 сходятся к 1.2~1.5×; в другой лиге остаются только SM2 (структурный разрыв, см. ниже) и SM3.

Воспроизведение:

bash
bash bench/build-cubelrti-wasm.sh                # конфигурация по умолчанию (-Oz)
SM_BUILD_O3=1 bash bench/build-cubelrti-wasm.sh  # скорость (-O3), для 4-го столбца выше
pnpm bench:cubelrti
Три наблюдения
  • Rust→WASM не обязательно быстрее: SM2 у Cubelrti проигрывает чистому JSsm-crypto-v2 по всем операциям (подпись — в 15× медленнее). Его горячий путь использует num-bigint (универсальные большие числа с выделением в куче) вместо арифметики фиксированной ширины; каждый вызов API заново выводит открытый ключ (его keypair — 2.20 ms, примерно половина sign — 4.56 ms, то есть в подписи выполняется два скалярных умножения); к тому же каждая экспортируемая функция вызывает console::log_1.
  • sm-crypto-v2 — потолок чистого JS: на SM4 он примерно наравне с Cubelrti, а на SM3 отстаёт от этого проекта всего в 1.35×.
  • Для массовых подписей есть отдельный путь: sm2.createSignaturePointPool(n) амортизирует умножение на базовую точку (56% времени подписи) заранее, снижая одну подпись с 141 µs до 24 µs (в 5.9×) при попадании в пул.

Почему так ​

  • SM3 получил ещё один раунд: compress раньше материализовал W′ (W[j] ^ W[j+4]) отдельным проходом из 64 записей + 128 чтений; теперь это вычисляется на месте внутри цикла раундов. Циклы раундов используют инкремент указателей, а буфер дополнения переиспользуется на уровне модуля (раньше каждый вызов hash делал new StaticArray<u8>(totalLen) и обнулял всю длину, тогда как обнулять нужно только область padding). HMAC целиком перенесён в WASM (склейка ipad/opad и два вызова хеширования раньше происходили в JS: четыре перехода через границу плюс три временных массива), для чего добавлен вход с двумя сегментами, позволяющий ipad ‖ msg попадать прямо в дополнение. Итог: hash быстрее на 26%, hmac — на 45%.
  • SM4 / ZUC упираются в S-бокс: оба ограничены табличным 8-битным S-боксом, а WebAssembly не предоставляет ни ARMv8 SM4E, ни AES-NI. SM4 пробовал v128 SIMD — вышло медленнее 8-кратного скалярного чередования (четыре независимые цепочки зависимостей на блок, скрывающие задержку L1 за счёт внеочередного исполнения); этот путь удалён. SM3 же — цепочка MD (блоки нельзя распараллелить) с последовательной компрессией в 64 раунда.
  • CFB / OFB в SM4 когда-то были медленнее чистого JS: эти режимы вызывали wasm поблочно (64 перехода на 1 КБ). Пакетные экспорты снизили их до ~10 µs, то есть в 3.6~3.8× быстрее.
  • Вычислительный слой SM2 прошёл девять этапов работы (редукция Солинаса, возведение в квадрат по Комба, comb/wNAF скалярное умножение, вычисления на точках на месте, смешанные координаты, совместное скалярное умножение при проверке, инверсия на уровне слов, кеш вывода открытого ключа для sign, заранее построенная comb-таблица для фиксированного открытого ключа). Подробности — в docs/performance-ceiling.md.
  • SM9 определяется спариваниями. Переход базового поля в домен Монтгомери (CIOS) снизил одно модульное умножение с 179 нс до 98 нс и ускорил все сквозные операции в 1.6~1.8×. Сопоставимой реализации SM9 на чистом JS нет, поэтому даны только абсолютные значения.
  • hex-кодеки важны: переход на Buffer.toString('hex') (с табличным запасным вариантом в браузерах) ускорил кодирование примерно в 27×; на входе проверка и разбор объединены в один проход. Вместе это ускорило SM3 / SM4 / ZUC с hex-входом на 27~32%.

Числа зависят от версии и машины

Значения выше получены на конкретной машине — используйте pnpm bench / pnpm bench:cubelrti на своей. Полный журнал экспериментов и оценка «сколько ещё осталось» — в docs/performance-ceiling.md.

Лицензия Apache-2.0