Производительность
Горячие участки (модульная арифметика больших чисел, компрессия хеша, раунды блочного шифра, билинейные спаривания) выполняются внутри WASM. Таблица покрывает каждую публичную операцию; столбец gmkitx — эталон на чистом JS на той же машине, с теми же параметрами и сообщениями (операций, которых там нет, помечены —). Измерено на AMD Ryzen 7 7800X3D / Node 24.15 / Windows 11; воспроизводится через pnpm bench.
| Алгоритм | Операция | sm-crypto-wasm | Чистый JS (gmkitx) | Отношение |
|---|---|---|---|---|
| SM3 | hash 32 B / 1 КБ / 64 КБ | 872 ns / 4.75 µs / 245 µs | 14.7 µs (1 КБ) | в 3.1× быстрее |
| SM3 | hmac 1 КБ | 5.58 µs | 19.4 µs | в 3.5× быстрее |
| SM4 | ECB 16 B / 1 КБ | 0.94 µs / 6.98 µs | 37.3 µs (1 КБ) | в 5.3× быстрее |
| SM4 | CBC 1 КБ | 10.5 µs | 41.6 µs | в 4.0× быстрее |
| SM4 | CTR 1 КБ | 7.43 µs | 36.5 µs | в 4.9× быстрее |
| SM4 | CFB 1 КБ | 10.0 µs | 37.9 µs | в 3.8× быстрее |
| SM4 | OFB 1 КБ | 9.98 µs | 36.3 µs | в 3.6× быстрее |
| SM4 | GCM 1 КБ | 19.8 µs | 221 µs | в 11.2× быстрее |
| SM2 | генерация пары ключей | 151 µs | — | — |
| SM2 | вывод открытого ключа | 110 µs | 197 µs | в 1.8× быстрее |
| SM2 | подпись 64 B | 142 µs | 446 µs | в 3.1× быстрее |
| SM2 | проверка 64 B | 402 µs | 2.85 ms | в 7.1× быстрее |
| SM2 | шифрование 64 B | 499 µs | 2.96 ms | в 5.9× быстрее |
| SM2 | расшифрование 64 B | 343 µs | 2.59 ms | в 7.6× быстрее |
| SM2 | KEM инкапсуляция / декапсуляция (16 B key) | 496 / 328 µs | — | — |
| SM2 | hybrid шифрование / расшифрование 1 MB | 37.3 / 36.4 ms | — | — |
| SM2 | обмен ключами (полный A↔B) | 1.52 ms | 11.4 ms | в 7.5× быстрее |
| SM9 | главная пара ключей (подпись / шифр) | 2.07 / 1.73 ms | — | — |
| SM9 | главный открытый ключ (подпись / шифр) | 97.1 / 49.1 µs | — | — |
| SM9 | вывод открытого ключа пользователя | 563 µs | — | — |
| SM9 | подпись 32 B | 4.11 ms | — | — |
| SM9 | проверка 32 B | 8.00 ms | — | — |
| SM9 | шифрование 32 B | 3.69 ms | — | — |
| SM9 | расшифрование 32 B | 5.40 ms | — | — |
| SM9 | KEM инкапсуляция / декапсуляция | 3.42 / 4.92 ms | — | — |
| SM9 | hybrid шифрование / расшифрование 10 KB | 3.21 / 4.77 ms | — | — |
| SM9 | обмен ключами (полный A↔B) | 19.6 ms | — | — |
| ZUC | EEA3 шифрование 1 КБ | 9.07 µs | 20.5 µs | в 2.3× быстрее |
| ZUC | EEA3 гамма 1 КБ | 6.15 µs | — | — |
| ZUC | EIA3 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-wasm | Cubelrti по умолч. (-Oz) | Cubelrti -O3 | sm-crypto-v2 | против Cubelrti (-Oz) | против Cubelrti (-O3) | против v2 |
|---|---|---|---|---|---|---|---|
| SM2 подпись 64 B | 143.6 µs | 4.51 ms | 3.03 ms | 297 µs | 31.4× | 21.1× | 2.07× |
| SM2 проверка 64 B | 406.6 µs | 4.49 ms | 3.05 ms | 2.83 ms | 11.0× | 7.50× | 6.96× |
| SM2 шифрование 64 B | 506.5 µs | 4.43 ms | 2.95 ms | 2.67 ms | 8.75× | 5.82× | 5.27× |
| SM2 расшифрование 64 B | 337.0 µs | 2.26 ms | 1.51 ms | 2.49 ms | 6.71× | 4.48× | 7.39× |
| SM2 пара ключей | 147.8 µs | 2.25 ms | 1.54 ms | 210 µs | 15.2× | 10.4× | 1.42× |
| SM3 hash 1 КБ | 4.75 µs | 24.7 µs | 11.5 µs | 6.44 µs | 5.19× | 2.42× | 1.36× |
| SM3 hmac 1 КБ | 5.61 µs | 29.3 µs | 13.7 µs | 9.27 µs | 5.22× | 2.44× | 1.65× |
| SM4 ECB шифрование 1 КБ | 6.78 µs | 12.6 µs | 8.83 µs | 12.6 µs | 1.85× | 1.30× | 1.85× |
| SM4 CBC шифрование 1 КБ | 10.4 µs | 19.5 µs | 12.5 µs | 14.9 µs | 1.88× | 1.21× | 1.44× |
| SM4 CBC расшифрование 1 КБ | 10.8 µs | 22.2 µs | 16.3 µs | 14.0 µs | 2.06× | 1.51× | 1.30× |
| SM4 CTR шифрование 1 КБ | 7.09 µs | 12.6 µs | 10.4 µs | — (не реализовано) | 1.77× | 1.46× | — |
| SM4 GCM шифрование 1 КБ | 19.9 µs | 27.5 µs | 19.7 µs | 30.4 µs | 1.39× | 0.99× | 1.53× |
Обратите внимание на последнюю строку: с -O3 в режиме SM4-GCM Cubelrti практически сравнивается с этим проектом. По операциям все режимы SM4 с -O3 сходятся к 1.2~1.5×; в другой лиге остаются только SM2 (структурный разрыв, см. ниже) и SM3.
Воспроизведение:
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 проигрывает чистому JS
sm-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 пробовалv128SIMD — вышло медленнее 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.