WebAssembly向け
多倍長演算の実装
2021/3/20
Kernel/VM探検隊online part2
光成滋生
• 背景
• ペアリング暗号
• 実装
• x64による多倍長演算の歴史
• wasmでの戦略
概要
2 / 21
• @herumi, https://github.com/herumi/
• Intelや富岳でのJITやAI関係の最適化
• https://gihyo.jp/news/interview/2020/11/1801
• 暗号とセキュリティに関するR&D ← 今日はこちら
自己紹介
3 / 21
• 暗号文の内積や署名の集約などが可能な高機能暗号
• 実装 https://github.com/herumi/mcl 世界最速 (今日の時点)
• Win/Linux/Mac/M1/Android/iPhone, Go/C#/Java/Rust binding
• 暗号文のまま積和演算 (L2準同型暗号)
• https://herumi.github.io/she-wasm
「ペアリング」を使った暗号技術 (1/2)
マンガ購入 購入せず
カメラ購入 * *
購入せず * *
暗号文を送信
テレビ マンガ 小説 カメラ
A * * * *
B * * * *
C * * * *
... * * * *
暗号文のままクロス集計
マンガ購入 購入せず
カメラ購入 80 20
購入せず 10 30
復号
クラウドサーバ
4 / 21
• 署名の秘密分散や集約 (BLS署名)
• https://github.com/herumi/bls
• EthereumJSのVMの暗号処理部分を担当
• https://github.com/ethereumjs/ethereumjs-monorepo
• その他、多数のブロックチェーンプロジェクトで採用
「ペアリング」を使った暗号技術 (2/2)
5 / 21
• 難しいのでスキップ
• 詳しい話は http://eprint.iacr.org/2014/401
• 秀和システム『クラウドを支えるこれからの暗号技術』
• http://herumi.github.io/ango ; PDF無料公開
ペアリングって何?
6 / 21
• ペアリングには楕円曲線が必要だ
• 楕円曲線には拡大体や有限体が必要だ
• 有限体には固定多倍長演算が必要だ
• 𝑥, 𝑦, 𝑝を384bit (or 256bit)整数として
• 𝑥 ± 𝑦 % 𝑝, 𝑥 ∗ 𝑦 % 𝑝 の計算を多数行う
• 足し算や掛け算ってどうやってするの? ← 今日の本題
• しばらくx86-64で解説
• 後半wasmでの方針を解説
• その前に記号
• 𝑥 = [𝑥3: 𝑥2: 𝑥1: 𝑥0]と書くと𝑥0, … , 𝑥3は符号無し64bit整数で
𝑥 = 𝑥3 ∗ 2192 + 𝑥2 ∗ 2128 + 𝑥1 ∗ 264 + 𝑥0を表すことにする
ブレイクダウン
7 / 21
• 64bitのレジスタ𝑎, 𝑏の和は65bit
• Intel CPUでadd 𝑎, 𝑏は𝑎 += 𝑏;の意味
• 残り1bitはCarryフラグ (CF)で表す ; [CF:a] = a+b
• [𝑦1: 𝑦0] + [𝑥1: 𝑥0]は
• add 𝑦0, 𝑥0 ; 𝑦0 += 𝑥0, CF= 0 or 1
• adc 𝑦1, 𝑥1 ; 𝑦1 += 𝑥1 +CF ; carryつきのadd
• AArch64ではaddsとadcs
2桁の足し算36 + 47 = ?
3 6
+ 4 7
-----
1 3
3
4
-----
8 3
繰り上がり
8 / 21
• 𝑦3: 𝑦2: 𝑦1: 𝑦0 += [𝑥3: 𝑥2: 𝑥1: 𝑥0]
• では73 * 7 = ?
• x64のmul命令は64x64→128
mul x ; [rdx:rax] ← x * rax
AArch64は上位・下位64bit取得のmul
mulとumulh
mulしてからcarryつきadd
4桁の足し算
add 𝑦0, 𝑥0
adc 𝑦1, 𝑥1
adc 𝑦2, 𝑥2
adc 𝑦3, 𝑥3
7 3 𝑦1 𝑦0
* 7 𝑥0
----- -------
2 1 |𝑦0 𝑥0|
4 9 |𝑦1 𝑥0|
----- ---------
5 1 1
繰り上がり
9 / 21
• mulで128bitのデータを順次足したい
• mul命令はCFを破壊する
• 8086時代からの流れ
• 128bit x 4個のmulをしてからでないとadd/adcできない
• レジスタを圧迫する
𝑦3: 𝑦2: 𝑦1: 𝑦0 ∗ 𝑥の問題点
𝑦0𝑥
𝑦1𝑥
𝑦2𝑥
𝑦3𝑥
10 / 21
• CFを破壊しない
• mov rdx, x / mulx H, L, y ; [H:L] = x * y
mulx登場 (from Haswell)
mov rdx, x ; rdx ← x
mulx z1, z0, y0
mulx z2, t, y1
add z1, t
mulx z3, t, y2
adc z2, t
mulx z4, t, y3
adc z3, t
adc z4, 0
[y3:y2:y1:y0]
* x
----------------------
[z1:z0]
[z2: t]
[z3: t]
[z4: t]
-----------------------
[z4:z3:z2:z1:z0]
11 / 21
• 途中に「𝑦𝑥𝑖−1 += 𝑦𝑥𝑖」
• 問題発生!
• 𝑦𝑥𝑖を計算するときにCFを使う
• 𝑦𝑥1の計算が終わらないと𝑦𝑥0に𝑦𝑥1を足せない
• レジスタ圧迫
[𝑦3: 𝑦2: 𝑦1: 𝑦0]*[𝑥3: 𝑥2: 𝑥1: 𝑥0]
[y3:y2:y1:y0]
x [x3:x2:x1:x0]
---------------------------------
[ y x0 ]
[ y x 1 ]
[ y x2 ]
[ y x3 ]
-----------------------------------
[z7:z6:z5:z4:z3:z2:z1:z0]
12 / 21
• 問題点はCFが1個しかないこと
• 2個あれば𝑦𝑥1を計算しながら𝑦𝑥0に足せる
• adcx ; CFつきの加算
• adox ; OFつきの加算
• CFとOFを独立した1bitレジスタとして扱う
• [z] += [y] * x
• [y] * xの計算にCFを使う
• [z] += の計算にOFを使う
• 中間レジスタ数低減
• BroadwellやRyzenで対応
adcx, adox登場
[ z ]
[y0 x1]
[y1 x1]
[y2 x1]
[y3 x1]
-----------------------------------
[z5:z4:z3:z2:z1:z0]
adcx
adox
13 / 21
• レジスタは32bit/64bit
• 加算は64bit + 64bit = 64bit ; carryが無い!
• 乗算は32bit * 32bit = 64bit ; 128bit出力が無い!
• 素直なCでできる範囲しかない
• https://webassembly.github.io/spec/core/binary/instructions.html
• carry演算をエミュレートしないといけない
• どうやって?
• 64bitのxとyを足してcarry発生
• ⇔ c = uint64_t(x+ y) < x
• lt_u x, y ; x < yなら1, そうでなければ0
• 他の比較に対する条件つきセット命令やselect命令も
さてwasmでは
14 / 21
• T = uint64_tとして
• 1個の要素あたり
• addが3回, lt_uが2回 (ループ変数の処理は除く)
• lt_uは内部的にはcmp + cmov (多分)
多倍長の足し算
T z = x + c;
c = z < c;
z += y;
c += z < y;
x T
y T
c bool
x + y
c
15 / 21
• T = uint32_t単位で処理する
• 32bitのx, yに対して64bitにzext (ゼロ拡張)する
• c = (x + y) >> 32;
• 1個の要素あたりaddが2回, シフトが1回, zextが2回
• 演算回数は増えるが条件代入は無くなる
• マイクロベンチマークでは気持ち64bit版が速い? (微妙)
• JITの影響も大きい
• 利点 : 32bit単位だとcが1bitより大きくてもよい
• まとめて足してからcarry操作をすることでステップ数削減
別の方法
v = uint64_t(x) + y + c;
z = T(v);
c = T(v >> 32);
x T
y T
c bool
x + y
c
16 / 21
• 256x256→512の乗算
• 64bit単位で処理したもの(64x64→128は32x32→64で実装)
• 32bit配列で処理したもの
• (先月試して)32bit単位で処理した方が速くなった
• 乗算がプロファイルの全体を占めるので効果が高い
• Q. Karatsuba法を使ったら?
• (aW + b)(cW + d) = acW^2 + ((a+b)(c+d) - ac - bd)W + bd
• 加算が重たいので256bitや384bitでは使わない方が速かった
ベンチマーク
mul256 mul384
64bit 196 339
32bit 168 275
単位nsec
Core i7 3.4GHz
17 / 21
• uint128_tやuint1024_tなどを作れる言語拡張
• typedef unsigned _ExtInt(256) uint256_t; などとして
• これだけでmul256やmul384が実装できる!!!
• 職人プログラマ不要になる?
• C++にも入れようという提案も
• https://github.com/herumi/misc/blob/master/cpp/p1889.md
clang-11の_ExtInt
void mul256(T *pz, const T *px, const T *py) {
uint256_t x = *px;
uint256_t y = *py;
uint512_t z = uint512_t(x) * y; // 符号拡張して乗算
*(uint512_t*)pz = z;
}
18 / 21
• x64でのベンチマーク
• mclのは職人技(?)による多分最速コード(w/ adox, adcx)
• clangはmulxまでしか使わない・レジスタスピルする
• wasmでは(--target=wam32)
• __multi3 is not found ; リンクできない
• clang-8の頃は自分で__multi3を実装して動いてたんだけどな
• 仕様が変わった? 未調査(昔はLLVMが落ちていた)
残念なところ
clang-11 -Ofast mcl
mul256 88.2 53.2
sqr256 82.1 41.4
wasm-ld-11: warning: function signature mismatch: __multi3
>>> defined as (i32, i64, i64, i64, i64) -> void in add.o
>>> defined as (i32, i64, i64) -> void in multi3.o
単位clk
Core i7 3.4GHz
19 / 21
• C/C++からwasmを生成するコンパイラ
emcc (Emscripten)とclang
emcc clang
標準ライブラリ 対応
mallocも使える
ヘッダが無い
(WASIを使う?)
関数export __attribute__((used
))
__attribute__(
(visibility("default")
))
exportの名前 先頭に'_'がつく '_'がつかない
便利機能 -s SINGLE_FILE=1
wasmをbase64で
埋め込んだjsを生成
LLVM IRを
直接扱える
安定度 (個人の感想) 割と枯れてきた 結構仕様が変わる
20 / 21
• 多倍長演算ではcarryの扱いが重要
• x64には便利な命令がいろいろ追加されている
• wasmはcarryを扱う命令が無い
• 素直にCで記述する範囲でしかできない or で十分
• clangの_ExtIntに期待
• 個人的にはemccがllvm/_ExtIntサポートするまで待ち?
まとめ
21 / 21

WebAssembly向け多倍長演算の実装

  • 1.
  • 2.
    • 背景 • ペアリング暗号 •実装 • x64による多倍長演算の歴史 • wasmでの戦略 概要 2 / 21
  • 3.
    • @herumi, https://github.com/herumi/ •Intelや富岳でのJITやAI関係の最適化 • https://gihyo.jp/news/interview/2020/11/1801 • 暗号とセキュリティに関するR&D ← 今日はこちら 自己紹介 3 / 21
  • 4.
    • 暗号文の内積や署名の集約などが可能な高機能暗号 • 実装https://github.com/herumi/mcl 世界最速 (今日の時点) • Win/Linux/Mac/M1/Android/iPhone, Go/C#/Java/Rust binding • 暗号文のまま積和演算 (L2準同型暗号) • https://herumi.github.io/she-wasm 「ペアリング」を使った暗号技術 (1/2) マンガ購入 購入せず カメラ購入 * * 購入せず * * 暗号文を送信 テレビ マンガ 小説 カメラ A * * * * B * * * * C * * * * ... * * * * 暗号文のままクロス集計 マンガ購入 購入せず カメラ購入 80 20 購入せず 10 30 復号 クラウドサーバ 4 / 21
  • 5.
    • 署名の秘密分散や集約 (BLS署名) •https://github.com/herumi/bls • EthereumJSのVMの暗号処理部分を担当 • https://github.com/ethereumjs/ethereumjs-monorepo • その他、多数のブロックチェーンプロジェクトで採用 「ペアリング」を使った暗号技術 (2/2) 5 / 21
  • 6.
    • 難しいのでスキップ • 詳しい話はhttp://eprint.iacr.org/2014/401 • 秀和システム『クラウドを支えるこれからの暗号技術』 • http://herumi.github.io/ango ; PDF無料公開 ペアリングって何? 6 / 21
  • 7.
    • ペアリングには楕円曲線が必要だ • 楕円曲線には拡大体や有限体が必要だ •有限体には固定多倍長演算が必要だ • 𝑥, 𝑦, 𝑝を384bit (or 256bit)整数として • 𝑥 ± 𝑦 % 𝑝, 𝑥 ∗ 𝑦 % 𝑝 の計算を多数行う • 足し算や掛け算ってどうやってするの? ← 今日の本題 • しばらくx86-64で解説 • 後半wasmでの方針を解説 • その前に記号 • 𝑥 = [𝑥3: 𝑥2: 𝑥1: 𝑥0]と書くと𝑥0, … , 𝑥3は符号無し64bit整数で 𝑥 = 𝑥3 ∗ 2192 + 𝑥2 ∗ 2128 + 𝑥1 ∗ 264 + 𝑥0を表すことにする ブレイクダウン 7 / 21
  • 8.
    • 64bitのレジスタ𝑎, 𝑏の和は65bit •Intel CPUでadd 𝑎, 𝑏は𝑎 += 𝑏;の意味 • 残り1bitはCarryフラグ (CF)で表す ; [CF:a] = a+b • [𝑦1: 𝑦0] + [𝑥1: 𝑥0]は • add 𝑦0, 𝑥0 ; 𝑦0 += 𝑥0, CF= 0 or 1 • adc 𝑦1, 𝑥1 ; 𝑦1 += 𝑥1 +CF ; carryつきのadd • AArch64ではaddsとadcs 2桁の足し算36 + 47 = ? 3 6 + 4 7 ----- 1 3 3 4 ----- 8 3 繰り上がり 8 / 21
  • 9.
    • 𝑦3: 𝑦2:𝑦1: 𝑦0 += [𝑥3: 𝑥2: 𝑥1: 𝑥0] • では73 * 7 = ? • x64のmul命令は64x64→128 mul x ; [rdx:rax] ← x * rax AArch64は上位・下位64bit取得のmul mulとumulh mulしてからcarryつきadd 4桁の足し算 add 𝑦0, 𝑥0 adc 𝑦1, 𝑥1 adc 𝑦2, 𝑥2 adc 𝑦3, 𝑥3 7 3 𝑦1 𝑦0 * 7 𝑥0 ----- ------- 2 1 |𝑦0 𝑥0| 4 9 |𝑦1 𝑥0| ----- --------- 5 1 1 繰り上がり 9 / 21
  • 10.
    • mulで128bitのデータを順次足したい • mul命令はCFを破壊する •8086時代からの流れ • 128bit x 4個のmulをしてからでないとadd/adcできない • レジスタを圧迫する 𝑦3: 𝑦2: 𝑦1: 𝑦0 ∗ 𝑥の問題点 𝑦0𝑥 𝑦1𝑥 𝑦2𝑥 𝑦3𝑥 10 / 21
  • 11.
    • CFを破壊しない • movrdx, x / mulx H, L, y ; [H:L] = x * y mulx登場 (from Haswell) mov rdx, x ; rdx ← x mulx z1, z0, y0 mulx z2, t, y1 add z1, t mulx z3, t, y2 adc z2, t mulx z4, t, y3 adc z3, t adc z4, 0 [y3:y2:y1:y0] * x ---------------------- [z1:z0] [z2: t] [z3: t] [z4: t] ----------------------- [z4:z3:z2:z1:z0] 11 / 21
  • 12.
    • 途中に「𝑦𝑥𝑖−1 +=𝑦𝑥𝑖」 • 問題発生! • 𝑦𝑥𝑖を計算するときにCFを使う • 𝑦𝑥1の計算が終わらないと𝑦𝑥0に𝑦𝑥1を足せない • レジスタ圧迫 [𝑦3: 𝑦2: 𝑦1: 𝑦0]*[𝑥3: 𝑥2: 𝑥1: 𝑥0] [y3:y2:y1:y0] x [x3:x2:x1:x0] --------------------------------- [ y x0 ] [ y x 1 ] [ y x2 ] [ y x3 ] ----------------------------------- [z7:z6:z5:z4:z3:z2:z1:z0] 12 / 21
  • 13.
    • 問題点はCFが1個しかないこと • 2個あれば𝑦𝑥1を計算しながら𝑦𝑥0に足せる •adcx ; CFつきの加算 • adox ; OFつきの加算 • CFとOFを独立した1bitレジスタとして扱う • [z] += [y] * x • [y] * xの計算にCFを使う • [z] += の計算にOFを使う • 中間レジスタ数低減 • BroadwellやRyzenで対応 adcx, adox登場 [ z ] [y0 x1] [y1 x1] [y2 x1] [y3 x1] ----------------------------------- [z5:z4:z3:z2:z1:z0] adcx adox 13 / 21
  • 14.
    • レジスタは32bit/64bit • 加算は64bit+ 64bit = 64bit ; carryが無い! • 乗算は32bit * 32bit = 64bit ; 128bit出力が無い! • 素直なCでできる範囲しかない • https://webassembly.github.io/spec/core/binary/instructions.html • carry演算をエミュレートしないといけない • どうやって? • 64bitのxとyを足してcarry発生 • ⇔ c = uint64_t(x+ y) < x • lt_u x, y ; x < yなら1, そうでなければ0 • 他の比較に対する条件つきセット命令やselect命令も さてwasmでは 14 / 21
  • 15.
    • T =uint64_tとして • 1個の要素あたり • addが3回, lt_uが2回 (ループ変数の処理は除く) • lt_uは内部的にはcmp + cmov (多分) 多倍長の足し算 T z = x + c; c = z < c; z += y; c += z < y; x T y T c bool x + y c 15 / 21
  • 16.
    • T =uint32_t単位で処理する • 32bitのx, yに対して64bitにzext (ゼロ拡張)する • c = (x + y) >> 32; • 1個の要素あたりaddが2回, シフトが1回, zextが2回 • 演算回数は増えるが条件代入は無くなる • マイクロベンチマークでは気持ち64bit版が速い? (微妙) • JITの影響も大きい • 利点 : 32bit単位だとcが1bitより大きくてもよい • まとめて足してからcarry操作をすることでステップ数削減 別の方法 v = uint64_t(x) + y + c; z = T(v); c = T(v >> 32); x T y T c bool x + y c 16 / 21
  • 17.
    • 256x256→512の乗算 • 64bit単位で処理したもの(64x64→128は32x32→64で実装) •32bit配列で処理したもの • (先月試して)32bit単位で処理した方が速くなった • 乗算がプロファイルの全体を占めるので効果が高い • Q. Karatsuba法を使ったら? • (aW + b)(cW + d) = acW^2 + ((a+b)(c+d) - ac - bd)W + bd • 加算が重たいので256bitや384bitでは使わない方が速かった ベンチマーク mul256 mul384 64bit 196 339 32bit 168 275 単位nsec Core i7 3.4GHz 17 / 21
  • 18.
    • uint128_tやuint1024_tなどを作れる言語拡張 • typedefunsigned _ExtInt(256) uint256_t; などとして • これだけでmul256やmul384が実装できる!!! • 職人プログラマ不要になる? • C++にも入れようという提案も • https://github.com/herumi/misc/blob/master/cpp/p1889.md clang-11の_ExtInt void mul256(T *pz, const T *px, const T *py) { uint256_t x = *px; uint256_t y = *py; uint512_t z = uint512_t(x) * y; // 符号拡張して乗算 *(uint512_t*)pz = z; } 18 / 21
  • 19.
    • x64でのベンチマーク • mclのは職人技(?)による多分最速コード(w/adox, adcx) • clangはmulxまでしか使わない・レジスタスピルする • wasmでは(--target=wam32) • __multi3 is not found ; リンクできない • clang-8の頃は自分で__multi3を実装して動いてたんだけどな • 仕様が変わった? 未調査(昔はLLVMが落ちていた) 残念なところ clang-11 -Ofast mcl mul256 88.2 53.2 sqr256 82.1 41.4 wasm-ld-11: warning: function signature mismatch: __multi3 >>> defined as (i32, i64, i64, i64, i64) -> void in add.o >>> defined as (i32, i64, i64) -> void in multi3.o 単位clk Core i7 3.4GHz 19 / 21
  • 20.
    • C/C++からwasmを生成するコンパイラ emcc (Emscripten)とclang emccclang 標準ライブラリ 対応 mallocも使える ヘッダが無い (WASIを使う?) 関数export __attribute__((used )) __attribute__( (visibility("default") )) exportの名前 先頭に'_'がつく '_'がつかない 便利機能 -s SINGLE_FILE=1 wasmをbase64で 埋め込んだjsを生成 LLVM IRを 直接扱える 安定度 (個人の感想) 割と枯れてきた 結構仕様が変わる 20 / 21
  • 21.
    • 多倍長演算ではcarryの扱いが重要 • x64には便利な命令がいろいろ追加されている •wasmはcarryを扱う命令が無い • 素直にCで記述する範囲でしかできない or で十分 • clangの_ExtIntに期待 • 個人的にはemccがllvm/_ExtIntサポートするまで待ち? まとめ 21 / 21