中程度の罠 – GMPのC++バインディング

WARNING

この記事ChatGPTによって中国語から翻訳されたものでいくつかのりがまれているかもしれません。不正確部分があればご了承ください

GNU MPライブラリC++バインディング

GNU MPライブラリ、大きな整数多精度浮動小数点数計算ライブラリですそれ自体C言語かれていますがC++バインディング提供していますC++プログラムくときあなたが自己虐待狂手動コンパイラ変換熱狂的愛好家でないならC++バインディング使うことは間違いなく選択です

これはC言語バージョンバインディングがすべての操作アセンブリ言語命令のようにカプセルしているからです。例えば、大きな整数バージョン 1+2 計算したい場合、次のようにくべきです

1mpz_t a, b, c;
2mpz_init_set_ui(a, 1);
3mpz_init_set_ui(b, 2);
4mpz_add(c, a, b);
5mpz_clear(a);
6mpz_clear(b);
7mpz_clear(c);

正直うとこれは直接アセンブリくよりも簡潔ではありません

1mov $1, %eax
2mov $2, %ebx
3add %ebx, %eax
4mov %ecx, %eax

この状況根本的原因C言語にはメモリリソース管理高速構造構築便利手段がないもちろん、新しい標準にはいくつかあるためC言語式評価モデル実装することができますがカスタム式評価モデル便利実装することはできません

式評価モデルレジスタマシンモデル変換コンパイル本質でありこのようにC言語コードくことは、部分的コンパイラ変換えばANF自分うことに相当しますだからこのようにコードくのがきなアセンブリくのがきな、手動コンパイラ変換うのがきなです

C++バインディングはこの時点救世主となりC/C++使用しなければならない場合えばたちの学校「現代暗号学実験」コースC++バインディング使用することでこのような困難けることができます

1const mpz_class a {1}, b {2};
2const auto c = a + b;

このコード、上記C言語コード完全動作実行しますこれはC++Cべていくつかのれた特性っているためでそのでもれているのはRAIIつまりResource Acquisition Is Initializationリソース取得初期化ですここでは、実行スタック一緒動作、簡単えばコンストラクタデストラクタわせによりスタックオブジェクト構築されるとき手動バインディング導入するときリソース取得、破棄されるとき現在スコープ退出するときに自動的破棄されるリソース解放しますメモリというリソースしてはこれにより我々GCのある言語』使用しているかのようにどんなメモリ問題にせずにむようになります

自然じるつの問題スタックRAII本当GCえることができるのかということです。以下GMP説明じて、読者さんが自分自身えをすことができるでしょう

奇妙問題

べたように、私GNU MPライブラリ使用する目的暗号学実験うためです。私たちの暗号学実験ではDLP離散対数計算問題がありその規模非常きく、実行速度重要要素ですそのため、私速度保証されているGNU MPのようなライブラリ使用しなければなりませんしかし、実験中、私非常奇妙問題遭遇しましたそれは、時々一部コード頻繁不正確結果すというもので、私何度コードチェックしても問題原因つけることができませんでしたさらに深刻なのはこれらの問題幽霊のように、時々現れたりえたりし、現れたときの結果時々なるということです

これにして、私最初反応メモリ問題があるのではないかということでしたしかし、私はすぐにこのえを否定しましたGNU MPのようなくの人々使用されているライブラリ、一般的にはこのような悪性問題こすことはありませんしかし、私コードには単純計算しかまれていませんでした。例えば

1/*
2 * Pohlig-Hellman algorithm for Group of prime power order
3 */
4mpz_class
5pohligHellmanP(const mpz_class& g, const mpz_class& h,
6               const mpz_class& pn, const mpz_class& en,
7               const mpz_class& p) {
8    const auto y = fastPow(g, Pow(pn, en - 1), p);
9    assert (fastPow(y, pn, p) == 1);
10    mpz_class x{0};
11    for (auto i = 0; i < en; ++i) {
12        auto hi = fastPow(Inverse(fastPow(g, x, p), p) * h,
13                          Pow(pn, en - 1 - i), p);
14        auto di = pDlp(y, hi, pn, p);
15        x = x + Pow(pn, i) * di;
16    }
17    return x;
18}

一連困難探求後、私『最小問題構造』確定しました。『最小問題構造』とはこの問題こす単純、行数ないコードしますそれはのようなものです

1mpz_class nothing() {
2  const auto a = mpz_class { 1 } + mpz_class { 2 };
3  std::cout << a << std::endl;
4  return a;
5}
6
7int main() {
8  std::cout << nothing();
9}

コンピュータではこのコード非常くべき結果します

1➜  gmp_error git:(master) ✗ g++ test.cpp -o a -g -lgmp -O0 -lgmpxx
2➜  gmp_error git:(master) ✗ ./a                                   
394361021124304
494361021124336%

1+2=943610211243041+2=94361021124304それとも 1+2=943610211243361+2=94361021124336

これほど単純コードがこれほど奇妙エラーじさせるなんて、本当奇妙です

GNU MPライブラリ設計

このくためには、別奇妙現象からをつけるべきですそれはのようなものです

1mpz_class nothing() {
2  const mpz_class a = mpz_class { 1 } + mpz_class { 2 };
3  std::cout << a << std::endl;
4  return a;
5}
6
7int main() {
8  std::cout << nothing();
9}

このコード問題がない読者さんはこの事実じるのがしいかもしれませんがそれは現実です

1➜  gmp_error git:(master) ✗ g++ test.cpp -o a -g -lgmp -O0 -lgmpxx
2➜  gmp_error git:(master) ✗ ./a                                   
33
43%

それにより問題明確になりますautoというキーワードa推論するのでしょうかIDEc++filt確認すると、答えはますます混乱します

1const __gmp_expr<mpz_t, __gmp_binary_expr<mpz_class, mpz_class, __gmp_binary_plus>> a

このですかえをつけるためにはgmpxx.hというファイル必要があるようです

gmpxx.hるとmpz_class実際にはmpz_expr<mpz_t, mpz_t>であることがわかります

1/**************** mpz_class -- wrapper for mpz_t ****************/
2
3template <> // line 1572
4class __gmp_expr<mpz_t, mpz_t>{ ... }; 
5
6typedef __gmp_expr<mpz_t, mpz_t> mpz_class; // line 1756

ではこの__gmp_exprという高階型理論的にはかに高階型相当しますにも特化があるのでしょうかかにこのファイルには__gmp_exprくの特化定義されています。例えば、先ほどa実際以下のようになります

1template <class T, class Op>
2class __gmp_expr
3<T, __gmp_binary_expr<__gmp_expr<T, T>, __gmp_expr<T, T>, Op> >

このクラスコンストラクタてみましょう

1__gmp_expr(const val1_type &val1, const val2_type &val2)
2    : expr(val1, val2) { }

exprクラスメンバ変数、以下のように宣言されています

1__gmp_binary_expr<val1_type, val2_type, Op> expr;

この__gmp_binary_exprとは何者なのでしょうかその定義以下りです

1template <class T, class U, class Op>
2struct __gmp_binary_expr
3{
4  typename __gmp_resolve_ref<T>::ref_type val1;
5  typename __gmp_resolve_ref<U>::ref_type val2;
6
7  __gmp_binary_expr(const T &v1, const U &v2) : val1(v1), val2(v2) { }
8private:
9  __gmp_binary_expr();
10};

これは混乱くかもしれませんこのようなコンストラクタしかたないクラス定義する特別意味でしょうかそれを使用する関数つける必要があります。以前べたように、右辺mpz_classであれば問題発生しませんmpz_expr<..>からmpz_classになるとき、型変換われていますこの型変換関数はどこにあるのでしょうかmpz_class定義ります

1template <class T>
2__gmp_expr(const __gmp_expr<mpz_t, T> &expr)
3{ mpz_init(mp); __gmp_set_expr(mp, expr); }
4template <class T, class U>
5explicit __gmp_expr(const __gmp_expr<T, U> &expr)
6{ mpz_init(mp); __gmp_set_expr(mp, expr); }

この関数間違いなく__gmp_expr<...>mpz_class変換していますでは__gmp_set_exprをしているのでしょうか

その定義てみましょう

1template <class T>
2inline void __gmp_set_expr(mpz_ptr z, const __gmp_expr<mpz_t, T> &expr)
3{
4  expr.eval(z);
5}

このeval関数__gmp_expr<T ...>定義されているようです、先ほどの定義再度確認してみましょう

1void eval(typename __gmp_resolve_expr<T>::ptr_type p) const
2{ Op::eval(p, expr.val1.__get_mp(), expr.val2.__get_mp()); }

これはOp::eval関数転送されています。以前Op__gmp_binary_plusそのeval関数はどのように定義されているのでしょうか

1struct __gmp_binary_plus
2{
3  static void eval(mpz_ptr z, mpz_srcptr w, mpz_srcptr v)
4  { mpz_add(z, w, v); }

これは非常親切ついにこの一連コンボをしているのかを理解しました

まず__gmp_expr< ... >構文木のようなものですべての操作情報記録していますこのmpz_class変換されるとき、評価われ、評価後変換後バインド格納されます

しかしこれが意味するのでしょうか見解ではこのようなコードロジック簡略化していませんC++コンパイラ余分コピー生成しないことを完全保証できます。実際、このように複雑構造直接クラスいて演算子オーバーロードする効果はほぼじです

唯一利点、変数auto使用している場合、変数自体ではなく構文木でありその必要になるつまり型変換われるまで評価されないことですこれはいわゆる「遅延評価」です

数値計算タスク遅延評価利点なのか、私には理解できません。遅延評価最大利点、不必要計算しないことです。例えば

1(define (f) (f))
2(define (g t1 t2) (t2))
3
4(g (f) 1) ;schemeでは、無限ループ
1f = f
2g t1 t2 = t2
3g f 1 --haskellでは、これは 1 を返します

しかしこのような数値計算タスクでは、通常余計計算わない。遅延評価自体必要計算簡略化できずパフォーマンス利点はありません

さらにこの設計ほどの深刻エラーこしますこれは、各__gmp_binary_expr実際保存しているのは2つの変数const参照であり、基本的const参照右辺値キャプチャできないからです。呼

1__gmp_binary_expr(const T &v1, const U &v2) : val1(v1), val2(v2) { }

v1へのポインタval1v2へのポインタval2てるだけです

このコード再度見てみましょう

1const auto a = mpz_class { 1 } + mpz_class { 2 };
2...

実際にはのようになります

1mpz_class temp1 {1}, temp2 {2};
2a = temp1 + temp2;
3~temp1(); ~temp2();
4...

デストラクタ実行された後、a構文木ノード対象完全デストラクトされこれらのオブジェクトアクセスするコードはすべてエラーです。言えればa有効なのは現在完了、次がまだ実行されていない瞬間だけです

問題解決

問題解決するには2つの方法があります

  • gmpxx.h修正する
  • すべての宣言mpz_classauto使用しない

しかしこのファイル修正してもconst&右辺値キャプチャできない問題依然として解決できません

__gmp_binary_exprセマンティクス変更するのはどうでしょうかつまりval1val2const &Tconst &Uではなく、実際TUにするということです

これは const auto a = mpz_class { 1 } + mpz_class { 2 }; 問題みなく解決できますなぜならmpz_class{1}mpz_class{2}はどちらも「右値」、つまりX値」、「右値参照」によってリソースみなくぐことができるからです。実際、加法問題解決するだけならいくつかの箇所修正するだけでみます

1/* 修改这个宏,使得加法有右值引用的版本 */
2#define __GMPP_DEFINE_BINARY_FUNCTION(fun, eval_fun)                   \
3                                                                       \
4template <class T, class U, class V, class W>                          \
5inline __gmp_expr<typename __gmp_resolve_expr<T, V>::value_type,       \
6__gmp_binary_expr<__gmp_expr<T, U>, __gmp_expr<V, W>, eval_fun> >      \
7fun(const __gmp_expr<T, U> &expr1, const __gmp_expr<V, W> &expr2)      \
8{                                                                      \
9  return __gmp_expr<typename __gmp_resolve_expr<T, V>::value_type,     \
10     __gmp_binary_expr<__gmp_expr<T, U>, __gmp_expr<V, W>, eval_fun> > \
11    (expr1, expr2);                                                    \
12}                                                                      \
13template <class T, class U, class V, class W>                          \
14inline __gmp_expr<typename __gmp_resolve_expr<T, V>::value_type,       \
15__gmp_binary_expr<__gmp_expr<T, U>, __gmp_expr<V, W>, eval_fun> >      \
16fun(__gmp_expr<T, U> &&expr1, __gmp_expr<V, W> &&expr2)                \
17{                                                                      \
18  return __gmp_expr<typename __gmp_resolve_expr<T, V>::value_type,     \
19     __gmp_binary_expr<__gmp_expr<T, U>, __gmp_expr<V, W>, eval_fun> > \
20    (std::move(expr1), std::move(expr2));                              \
21}
1/* 修改这个类,使得构造函数有右值引用的版本 */
2template <class T, class Op>
3class __gmp_expr
4<T, __gmp_binary_expr<__gmp_expr<T, T>, __gmp_expr<T, T>, Op> >
5{
6private:
7  typedef __gmp_expr<T, T> val1_type;
8  typedef __gmp_expr<T, T> val2_type;
9
10  __gmp_binary_expr<val1_type, val2_type, Op> expr;
11public:
12  __gmp_expr(const val1_type &val1, const val2_type &val2)
13    : expr(val1, val2) { } 
14  __gmp_expr(val1_type &&val1, val2_type &&val2) // 新加入的构造函数
15    : expr(std::move(val1), std::move(val2)) { }
1template <class Op>
2struct __gmp_binary_expr<mpz_class, mpz_class, Op>
3{
4  mpz_class val1;
5  mpz_class val2;
6  __gmp_binary_expr(const mpz_class &v1, const mpz_class &v2) 
7    : val1(v1), val2(v2) { }
8  __gmp_binary_expr(mpz_class &&v1, mpz_class &&v2) 
9    : val1(std::move(v1)), val2(std::move(v2)) { }
10private:
11  __gmp_binary_expr();
12};

__gmp_binary_expr特化自分定義、両方mpz_class場合処理します

これにより、上記コードしく3ることができます

しかし、修正完了するまでの作業量はさておきこのように修正するとつの問題直面します左値された場合、痛みなく移動することはできずコピー必要がありこれはパフォーマンスにとって不利です

この問題をどのように解決するのでしょうかえはなくともには解決できません

GCRAII

上記問題GCがある言語では、簡単えば問題ではありませんたとえばPythonのような言語ではリソースバインドしてもコピー発生しません

1a = [1, 2, 3, 4]
2b = a
3c = b

もちろんこれはabc実際にはオブジェクトしているためでconst &のようなものです

しかしGCがある言語ではconst &「右値キャプチャできないという問題完全解決できます

1class A:
2    def __init__(self, arr):
3        self.arr = arr
4        
5a = A([1,2,3,4])

根本的にはRAIIではスタックオブジェクト2つのバインド同時「所有」することはできずスタックオブジェクト消去されるルール厳格スコープルールであり、「スタックからオブジェクトりるという状況発生しません。一方、GCがある言語では、「オブジェクトオブジェクト所有するリソース堆上にありさらに一体化しているためこの問題発生しません

このようにるとRAIIGCえることはできませんもちろんRustなどの言語では、他方法でこの問題解決できるかもしれませんしかし、結論としてC++ではRAII能力結局のところ限定的です