Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

optimal Ate pairing

1,741 views

Published on

introduction of x64 assembler for implementation of optimal ate pairing

Published in: Technology
  • Be the first to comment

optimal Ate pairing

  1. 1. Optimal Ateペアリングの 実装詳細 2013/7/3 サイボウズ・ラボ 光成滋生
  2. 2. 目次  Optimal Ateペアリングの定義(さらっと)  今回はペアリングの話ではなく最適化全般のトピックが主  x64 CPUの概略  実行時間の計測  整数加算、減算の実装  整数乗算の実装  Haswell向けの改良  その他のトピック / 282
  3. 3. BN曲線  𝐹𝑝上で定義される埋め込み次数12の楕円曲線  𝐸: 𝑦2 = 𝑥3 + 𝑏, 𝑏 ∈ 𝐹𝑝  𝑝 ≔ 𝑝 𝑧 = 36𝑧4 + 36𝑧3 + 24𝑧2 + 6𝑧 + 1 𝑧が64bitなら𝑝は256bitぐらいの素数  𝑟 ≔ 𝑟 𝑧 = 36𝑧4 + 36𝑧3 + 18𝑧2 + 6𝑧 + 1  𝑡 ≔ 𝑡 𝑧 = 6𝑧2 + 1  #𝐸(𝐹𝑝) = 𝑟 / 283
  4. 4. 記号  𝜋: 𝑥, 𝑦 → 𝑥 𝑝 , 𝑦 𝑝  Frobenius写像  BN曲線に対してはtrace(𝜋 𝑝) = 𝑡  𝑓𝑠,𝑄: 𝐸上の有理関数(𝑠は整数𝑄は𝐸上の点)  div 𝑓𝑠,𝑄 = 𝑠 𝑄 − 𝑠 𝑄 − 𝑠 − 1 𝒪 を満たすもの 𝑠 𝑄 は 𝑄 の形式的な𝑠倍、 𝑠 𝑄は𝑄の 𝑠 倍された点を意味する  𝑙 𝑄1,𝑄2  𝑄1と𝑄2を通る直線 / 284
  5. 5. Optimal Ateペアリング  𝐺1 = 𝐸 𝑟 ∩ ker 𝜋 𝑝 − 1 = 𝐸 𝐹𝑝 𝑟  𝐺2 = 𝐸 𝑟 ∩ ker 𝜋 𝑝 − 𝑝 ⊆ 𝐸 𝐹𝑝 12 𝑟  𝐺3 = 𝜇 𝑟 ⊂ 𝐹𝑝 12 ∗  𝑒 ∶ 𝐺2 × 𝐺1 ∋ 𝑄, 𝑃 ⟼ 𝑚 𝑄, 𝑃 𝑝12−1 𝑟 ∈ 𝐺3  𝑚 𝑄, 𝑃 ∶= 𝑓6𝑧+2,𝑄 𝑃 ∙ 𝑔 𝑄 𝑃  𝑔 𝑄(𝑃) ≔ 𝑙 6𝑧+2 𝑄,𝜋 𝑝 𝑄 𝑃 ∙ 𝑙 6𝑧+2 𝑄+𝜋 𝑝 𝑄 ,−𝜋 𝑝 2 𝑄 (𝑃) / 285
  6. 6. ペアリングのアルゴリズム  1) 6𝑧 + 2 𝑄 と 𝑓6𝑧+2,𝑄 𝑃 を算出(Millerループ)  2) 𝑚 𝑄, 𝑃 = 𝑓6𝑧+2,𝑄 𝑃 ∙ 𝑔 𝑄 𝑃 を算出  3) 𝑝12−1 𝑟 乗する(最終巾) / 286
  7. 7. 拡大体上の演算における戦略  𝐹 𝑝2上の乗算  x=a+bu, y = c+du, u^2 = -1  xy = (ac – bd) + ((a+b)(c+d) – ac – bd)u  従来  ac, bd, (a+b)(c+d)はFp:mulを使う  Pairing2010における主要アイデア  Fp:mul = mul256 + mod512 mul256 : 256ビット整数乗算mul256  64bit乗算命令は速い(3clk, latency, 1clk throughput) mod512 : Montgomeryリダクション  mul256の結果に対する加減算 ac, bd, (a+b)(c+d)を512bit整数のまま加減算 mod512の回数が3回から2回になる  512bit加減算は増える / 287
  8. 8. Aranhaらによる改良  𝐹 𝑝6などの拡大体にも容易に適用可能  拡大体の係数もより小さいものに  𝑏 = 2, z = −(262 + 255 + 1)  𝐹 𝑝2 = 𝐹𝑝 U / U2 − 𝛽 , 𝛽 = −1 ∈ 𝐹𝑝  𝐹 𝑝6 = 𝐹 𝑝2 V / V3 − 𝜉 , 𝜉 = 1 + U ∈ 𝐹 𝑝2  𝐹 𝑝12 = 𝐹 𝑝6 W / W2 − V , 𝛾 = 𝑉 ∈ 𝐹 𝑝6  実装  最新の実装は上記を踏襲し,細部を改良  https://github.com/herumi/ate-pairing/ 0.35msec@Haswell(i7-4700MQ 3.4GHz) / 288
  9. 9. x64 CPU概略  15個の汎用64bitレジスタ  rax, rbx, rcx, rdx, rsi, rdi, rbp, r8, r9, ..., r15  フラグレジスタ  演算結果に応じて変わる1bitの情報群 CF : 加算時に結果が64bitを超えた、減算でマイナスになった ZF : 結果が0になった SF : 結果の最上位ビットが1だった  呼び出し規約  関数の引数に対応するレジスタ名 WindowsとLinuxで異なる  Windows : rcx, rdx, r8, r9  Linux : rdi, rsi, rdx, rcx  関数の中で壊してよいものと元に戻す必要のあるもの Linux : r12, ..., r15, rbx, rbp, Win : 加えてrsi, rdi / 289
  10. 10. 算術演算  加減算  add x, y // x ← x + y;  sub x, y // x ← x – y;  carryつき加減算  adc x, y // x ← x + y + CF; 繰り上がりを加味  sbb x, y // x ← x – y – CF; 繰り下がりを加味  乗算  64bit x 64bit → 128bit  mul x // [rdx:rax] ← x * rax (rax, rdxレジスタ固定)  除算  128bit / 64bit = 64bit あまり 64bit  div x // [rdx:rax] / x ; 商 : rax, あまり : rdx / 2810
  11. 11. 条件比較  演算結果に応じてフラグが変わる  フラグに応じて条件分岐する  こういうコードはこんな感じ  jg (jmp if greater), jge(jmp if greater or equal)などなど / 2811 if (x >= y) { Aの作業 } else { Bの作業 } cmp x, y // x-yの計算結果をCFに反映(CF = x >= y ? 0 : 1) jnc LABEL_A // jmp to LABEL_A if no carry Bの作業 jmp NEXT LABEL_A: Aの作業 NEXT:
  12. 12. アセンブラの種類  gas, NASM, MASMなど  静的なアセンブラ  マクロや条件式などの文法はそれぞれ独自構文  inline assembler  おもにgcc(64bit Visual Studioでは非サポート)  コンパイラが多少最適化してくれることも  記述が難しい  LLVM  抽象度の高いアセンブラ/JIT可能  carryの扱いが難しく今回の用途では性能を出しにくい  Xbyak(拙作)  抽象度は低い(gasやNASMと同じ)/JIT可能  C++の文法でアセンブラをかける / 2812
  13. 13. 実行時間の測り方  Vtune(Intel), CodeAnalyst(AMD)など  CodeAnalystは無料 Intel CPUでも使える  perfコマンド(Linux only)  perf listで測定したいパラメータを表示 instructions branch-missessなどCPUによって様々なものがある  perf stat –e L1-icache-load-misses 実行コマンド / 2813
  14. 14. rdtsc  CPUがもつカウンタ  (2.8GHzなら1/2.8 nsec単位で)一つずつ増える  Turboboostは切った方が周波数が固定になってよい 駄目なら重たい処理を先に実行させてトップスピードにさせる  マルチプロセス向けにrdtscpというのもある  Xbyakではrdtscの薄いラッパークラスClockを提供  clk.begin(), clk.end()で測定したい関数をはさむ  最後にclk.getClock() / clk.getCount()で平均値を取得 / 2814 Xbyak::util::Clock clk; for (int i = 0;i < N; i++) { clk.begin(); some_function(); clk.end(); } printf("%.2fclk¥n", clk.getClock() / double(clk.getCount()));
  15. 15. 256bit加算  記法  xi, yi, ziなどは64bitレジスタを表す  [x3:x2:x1:x0]で256bit整数を表す(x0が最下位の64bit)  256bit整数z[]に256bit整数x[]を足すコードは次の通り  注意  z[], x[]が256bitフルに入ってると結果が257bitになる  今回はpを254bitに選んだため0 <= x, z < pならあふれない 他にも様々な箇所で桁あふれがおきないため処理の簡略化が可能 そのためセキュリティレベルが128bitではなく127bit / 2815 // [z3:z2:z1:z0] += [x3:x2:x1:x0] add z0, x0 adc z1, x1 // carryつき adc z2, x2 // carryつき adc z3, x3 // carryつき
  16. 16. 256bit加算を関数にする  呼び出し規約にしたがってレジスタを使う  なかなか面倒  XbyakのStackFrameを使うとある程度抽象化、自動化可能 LLVMはより汎用的にできる / 2816 //addNC(uint64_t z[4],const uint64_t x[4],const uint64_t y[4]); void gen_AddNC() { Xbyak::util::StackFrame sf(this, 3); //引数3個の関数 const Xbyak::Reg64& z = sf.p[0]; // 一つ目の引数 const Xbyak::Reg64& x = sf.p[1]; // 二つ目の引数 const Xbyak::Reg64& y = sf.p[2]; // 三つ目の引数 mov(rax, ptr [x]); add(rax, ptr [y]); mov(ptr [z], rax); for (int i = 1; i < 3; i++) { mov(rax, ptr [x + i * 8]); adc(rax, ptr [y + i * 8]); mov(ptr [z + i * 8], rax); } }
  17. 17. gen_addNCの結果  WindowsとLinuxのそれぞれに応じたコード生成  StackFrameはスタックを確保したり一時変数を使ったり、 rcx, rdxを特別扱いする指定もできる 自動的にレジスタの退避復元をおこなう / 2817 // Windows(引数はrcx,rdx,r8の順) mov rax,ptr [rdx] add rax,ptr [r8] mov ptr [rcx],rax mov rax,ptr [rdx+8] adc rax,ptr [r8+8] mov ptr [rcx+8],rax mov rax,ptr [rdx+10h] adc rax,ptr [r8+10h] mov ptr [rcx+10h],rax ret // Linux(引数はrdi,rsi,rdxの順) mov rax,ptr [rsi] add rax,ptr [rdx] mov ptr [rdi],rax mov rax,ptr [rsi+0x8] adc rax,ptr [rdx+0x8] mov ptr [rdi+0x8],rax mov rax,ptr [rsi+0x10] adc rax,ptr [rdx+0x10] mov ptr [rdi+0x10],rax ret
  18. 18. Fp::addの実装  addNCした結果zがz>=pならばpを引く  if (z >= p) z -= p;  アセンブラレベルでの比較の方法  z=[z3:z2:z1:z0]とx=[x3:x2:x1:x0]はどちらが大きいか  1. 頭から比較する  分岐がきわめて多くなる 連続する分岐命令は好まれない  2. 引いてから考える  分岐は1回 / 2818 cmp z3, x3 ja z_gt_x // z3 > x3 jb otherwise // z3 < x3 cmp z2, x2 // here z3 == x3 ja z_gt_x // z2 > x2 jb otherwise // z2 < x2 ... z_gt_x: ... otherwise:tmp_z = z // zの値を退避(mov x 4) subNC z, p // 引いてみる(z -= p) jnc .next // z >= 0ならnextへ z = tmp_z // zの値を復元(mov x 4) .next:
  19. 19. 分岐しないFp::addの実装  CPUは分岐予測をする  当たると大体1clk  外れると大体20clk  一般のデータでは偏りがあるので結構精度よく当たる  が、今回はランダムなので的中率は50%→平均10clk  分岐予測を排除する  条件移動命令cmovXX 2clk latency 1clk thrgouthput  addの二つの実装 分岐あり1.39Mclk, 分岐なし1.35Mclk もちろんCPUによって異なる可能性あり(sandy, ivyで効果あり) 単純ベンチだと分岐予測があたって分岐あり版が速くみえるかも / 2819 mov ti, zi x 4 subNC z, p cmovc zi, ti ; 引きすぎてたら戻す
  20. 20. Fp::subの実装  subNCした結果が負ならpを足す  addと違ってsubNCした時のCFを見ればよいので比較不要  分岐を使った実装  cmovを使った実装  0クリア  cmov + メモリロード  加算 結構命令数が多いので分岐に対してそれほどメリットがない  cmovを使わない実装  命令数は同じだが cmovよりは速い@sandy / 2820 // z -= xの直後 jnc .next z[] += p[] .next: t[] = 0 cmovc t[] p[] //t[] = CF ? p[0] : 0 z[] += t[] sbb t, t // t = CF ? -1 : 0 and t[], p[] // t = CF ? p : 0 z[] += t[]
  21. 21. 256ビット加減算の命令順序  メモリから読んで演算する二つの方式  方式A(メモリまとめ読み) 方式B(メモリと演算を交互に)  実験によるとどちらが速いかCPUにより異なる Opteron, i7は方式Aが速い Westmereは方式Bが速かった  out of orderだから関係ないと思ったが1%弱違った  実行時のCPU判別によりいずれかを選択 上記方式はコード全般にわたって適用される / 2821 z0 ← x[0] z1 ← x[1] z2 ← x[2] z3 ← x[3] z0 ← z0 + y[0] z1 ← z1 + y[1] with carry z2 ← z2 + y[2] with carry z3 ← z3 + y[3] with carry z0 ← x[0] z0 ← z0 + y[0] z1 ← x[1] z1 ← z1 + y[1] with carry z2 ← x[2] z2 ← z2 + y[2] with carry z3 ← x[3] z3 ← z3 + y[3] with carry
  22. 22. 256ビットx256ビット乗算(1/2)  256ビット整数を64ビット整数4個の組で表現する  64ビット→320ビット乗算4回と320ビット加算3回  筆算方式でmulするごとにaddを行う 繰り上がり加算が3回余計に増える / 2822 𝑥3 𝑥2 𝑥1 𝑥0 𝑦 𝑥0 𝑦 𝑥1 𝑦 + +(繰り上がり) 𝑥2 𝑦 + +(繰り上がり) ・・・ 1. 𝑥0 𝑦を計算 2. 𝑥1 𝑦を計算 3. 𝑥0 𝑦 𝐿 + 𝑥1 𝑦 𝐻 t0 = 𝑥1 𝑦 𝐻 + 𝑐𝑎𝑟𝑟𝑦 4. 𝑥2 𝑦を計算 5. 𝑥2 𝑦 𝐿 + 𝑡0 𝑡1 = 𝑥2 𝑦 𝐻 + 𝑐𝑎𝑟𝑟𝑦 6. ...
  23. 23. 256ビットx256ビット乗算(2/2)  乗算4回してから加算すると余計な加算が不要  ただし乗算結果を保持するワークエリアが必要  mulがCFを変更するためadcと同時に使えない  15個のレジスタを使い回して一時メモリを使わずに実装 / 2823 1. [𝑥𝑖 𝑦](𝑖 = 0 … 3)を計算 2. それらをまとめて加算 ↓ 加算は4回 𝑥3 𝑥2 𝑥1 𝑥0 𝑦 𝑥0 𝑦 𝑥1 𝑦 𝑥2 𝑦 𝑥3 𝑦 加算が終わるまでどこかに 保持する必要がある
  24. 24. 256ビットx256ビット乗算 for Haswell  HaswellではCFを変更しないmulxが導入された  加算(add, adc)しつつ乗算を繰り返しおこなえる  必要なレジスタ数が減る  退避、復元のためのmov命令が減る  Montgomery reductionにも適用可能  ペアリング全体で13%の高速化  1.33Mclkから1.17Mclkへ(@Core i7 4700MQ 2.4GHz) / 2824 mov(a, ptr [py]); | ↓ mul(x); | mul(x); mov(t0, a); | mov(t3, a); mov(t1, d); | mov(a, x); mov(a, ptr [py + 8]); | mov(x, d); mul(x); | mul(qword [py + 24]); mov(t, a); | add(t1, t); mov(t2, d); | adc(t2, t3); mov(a, ptr [py + 16]);| adc(x, a); ↓ | adc(d, 0); mov(d, x); mulx(t1, t0, ptr [py]); mulx(t2, a, ptr [py + 8]); add(t1, a); mulx(x, a, ptr [py + 16]); adc(t2, a); mulx(d, a, ptr [py + 24]); adc(x, a); adc(d, 0);
  25. 25. 記述の簡便さのための手法  各種2項演算はsrc x 2 + dstのglobal関数を作る  Fp::add(z, x, y); // z = x + yなど  &z == &x == &yなどのときでも正しく動くように注意  演算子オーバーロード  Fp operator+(const Fp&, const Fp&)などをFp::addを使 って定義する z = x + y;などとかける。  Fp2, Fp6, Fp12などの拡大体でも同様に作る  コピペばかりになって間違いやすい / 2825
  26. 26. CRTPによる半自動的生成手法  add, subなどを使ってoperator+, operator-を定義 するtemplateクラス  Fp, Fp2などはadd, subさえつくればaddsubmulを 継承することでoperator+が使えるようになる  virtual継承ではないので呼び出し時のコストは(通常)ない / 2826 template<class T, class E = Empty<T> > struct addsubmul : E { template<class N> T& operator+=(const N& rhs) { T::add(static_cast<T&>(*this), static_cast<T&>(*this), rhs); return static_cast<T&>(*this); } ... strut Fp : addsubmul<Fp>{ static void add(Fp&, const Fp&, const Fp&); };
  27. 27. 記法の簡便さと演算性能  z = x + y;とFp::add(z, x, y);  一般的に前者の方が書きやすく可読性も高い  しかし隠れた一時変数の生成とコピーに注意する  x + yの結果をtmpに保存 してz = tmpを実行  方針  最初は数式を書きやすい 前者で始める  動くことがわかったら 一時変数や移動を減らす ように後者に置き換える 式Templateによる一時変数 除去テクニックはあるが 正直使いにくい、挙動を 把握しにくいため勧めない / 2827 // Fp::add(z, x, y); lea r8,[y] lea rdx,[x] lea rcx,[z] call [mie::Fp::add] // z = x + y; lea r8,[rbp+7] lea rdx,[rbp-19h] lea rcx,[rbp-39h] call [mie::Fp::add] movaps xmm0,[rbp-39h] //データ移動 movaps [rbp+37h],xmm0 movaps xmm1,[rbp-29h] movaps [rbp+47h],xmm1
  28. 28.  Fp6などの演算は基礎体のmulやaddを呼び出す  mulはレジスタをフルに使うため関数の中でレジスタの退避 と復元をおこなっている  連続してmulを呼び出すならその間の復元と退避は除去可能  退避復元をしない専用関数を用意する 呼び出し規約からの逸脱  コンパイラの関知しないところのため手作業が必要 LLVMがこの分野で使えるならoptに任せることも可能になるか  メリット  速度向上  デメリット  デバッグが難しい かもしれない Fp2_mul: call Fp_mul call Fp_mul ret Fp_mul: レジスタの退避 本体 レジスタの復元 レジスタの退避・復元の省略の一般論 / 2828 Fp2_mul: call in_Fp_mul call in_Fp_mul ret // Cからは呼べない in_Fp_mul: 本体

×