paint-brush
真の C++ キラー (Not You, Rust)@oleksandrkaleniuk
50,471 測定値
50,471 測定値

真の C++ キラー (Not You, Rust)

Oleksandr Kaleniuk17m2023/02/14
Read on Terminal Reader

長すぎる; 読むには

悪いプログラマーで良いコードを書くことは、20 世紀の問題です。現在、より優れたコードが必要ですが、優れたプログラマーによって書かれており、現在の C++ キラーが対応していないタスクです。本当の革命はコンパイラの先にあります。
featured image - 真の C++ キラー (Not You, Rust)
Oleksandr Kaleniuk HackerNoon profile picture


こんにちは!私は Oleksandr Kaleniuk で、C++ 中毒です。私は 17 年間 C++ で書いてきましたが、その 17 年間ずっと、この壊滅的な中毒から抜け出そうと努力してきました。


すべては 2005 年に 3D スペース シミュレーター エンジンから始まりました。このエンジンには、2005 年の C++ に備わっていたすべての機能が備わっていました。3 つ星のポインター、8 層の依存関係、および C スタイルのマクロがいたるところにありました。組み立てビットもありました。イテレータ Stepanov スタイルとメタコード Alexandrescu スタイル。コードにはすべてがありました。もちろん、最も重要な質問への答えを除いて: なぜ?


しばらくすると、この質問にも答えが出ました。 「何のために」ではなく、「どうして」という意味で。結局のところ、エンジンは 5 つの異なるチームによって約 8 年間書かれてきました。そして、すべてのチームがお気に入りの流行をプロジェクトにもたらしました。古いコードを新しいスタイルのラッパーにラップし、その間に約 10 ~ 20 個のマイクロカーマックの機能を追加するだけでした。


最初は、正直に言って、あらゆる小さなことを理解しようとしていました。それはまったく満足のいく経験ではなく、ある時点であきらめました。私はまだタスクを閉じていて、バグを修正していました。私は非常に生産的だったとは言えませんが、クビにならない程度には十分でした。しかしその後、上司から「シェーダー コードの一部を Assembly から GLSG に書き直しますか?」と尋ねられました。この GLSL がどのように見えるかは神が知っていると思いましたが、C++ よりも悪いことはあり得ず、イエスと答えました。悪くはありませんでした。


と、こんなパターンになりました。私はまだほとんど C++ で書いていましたが、誰かに「C++ 以外のことをやりたいですか?」と尋ねられるたびに、私は確信していました!"そして、それが何であれ、私はそのことをしました。私は、C89、MASM32、C#、PHP、Delphi、ActionScript、JavaScript、Erlang、Python、Haskell、D、Rust、そしてとんでもなく悪い InstallShield スクリプト言語でさえ書きました。私は、VisualBasic、bash、およびいくつかの独自言語で書きましたが、法的に話すことさえできません。私もたまたま自分で作ったことがあります。ゲーム デザイナーがリソースのロードを自動化するのに役立つ単純な Lisp スタイルのインタープリターを作成し、休暇に出かけました。私が戻ったとき、彼らはこのインタプリタでゲーム シーン全体を書き込んでいたので、少なくともプロジェクトが終了するまでサポートしなければなりませんでした。


そのため、過去 17 年間、私は正直に C++ をやめようとしていましたが、毎回、新しい素晴らしいことを試した後、戻ってきていました。それでも、C++ で書くのは悪い習慣だと思います。それは安全ではなく、考えられているほど効果的ではなく、ソフトウェアの作成とは関係のないことにプログラマーの精神的能力をひどく浪費しています。 MSVC uint16_t(50000) + uin16_t(50000) == -1794967296でそれを知っていますか?なぜなのかご存知ですか?ええ、それは私が思ったことです。


若い世代が C++ を自分の職業にするのを思いとどまらせるのは、長年の C++ プログラマーの道徳的責任であると私は信じています。


でもなんでやめられないの?どうしたの?問題は、どの言語も、特にいわゆる「C++ キラー」が、現代の世界で C++ よりも真の利点を提供していないということです。これらの新しい言語はすべて、主にプログラマーを自らの利益のために縛り付けることに重点を置いています。これは問題ありませんが、トランジスタが 18 か月ごとに 2 倍に成長し、プログラマーの人数が 5 年ごとに 2 倍に増えた 20 世紀の問題は、悪いプログラマーで良いコードを書くことです。


私たちは 2023 年に生きています。歴史上かつてないほど経験豊富なプログラマーが世界中にいます。そして、今まで以上に効率的なソフトウェアが必要です。


20世紀には物事はより単純でした。アイデアがあれば、それを何らかの UI にラップして、デスクトップ製品として販売します。遅いですか?誰も気にしない!いずれにせよ、18 か月でデスクトップは 2 倍速くなるでしょう。重要なのは、市場に参入し、機能の販売を開始し、できればバグがないことです。そのような状況では、確かに、コンパイラーがプログラマーがバグを作成するのを防げるなら、それは良いことです!バグは現金をもたらさないため、機能やバグを追加するかどうかにかかわらず、プログラマーに支払う必要があります。


今は状況が異なります。アイデアがあれば、それを Docker コンテナーにラップして、クラウドで実行します。これで、問題が解消されれば、ソフトウェアを実行している人々から収益を得ることができます。たとえ 1 つのことを正しく行ったとしても、報酬を得ることができます。新しいバージョンを販売するためだけに、製品に独自の機能を詰め込む必要はありません。一方、コードの非効率性の代償を払うのは、今やあなた自身です。最適ではないすべてのルーチンが AWS の請求書に表示されます。


そのため、新しい環境では、必要な機能は少なくなりましたが、得たものに対してより優れたパフォーマンスが必要になりました.


そして突然、Rust、Julia、D のように私が心から愛し尊敬している「C++ キラー」でさえ、21 世紀の問題に対処していないことが判明しました。彼らはまだXXで立ち往生しています。それらは、より少ないバグでより多くの機能を作成するのに役立ちますが、レンタルしたハードウェアから最後のフロップを絞り出す必要がある場合には、あまり役に立ちません。


それらは、C++ に対する競争上の優位性を提供しません。または、さらに言えば、お互いにさえ。それらのほとんど、たとえば Rust、Julia、Cland は同じバックエンドを共有しています。全員が同じ車を共有していては、カーレースに勝つことはできません。


では、どのテクノロジーが C++ や、一般的に言えば従来のすべての事前コンパイラーに対して競争上の優位性をもたらすのでしょうか?良い質問。よろしくお願いします。


C++キラーナンバー1.スパイラル

しかし、Spiral 自体について説明する前に、あなたの直感がどれだけうまく機能するかを確認してみましょう。標準の C++ 正弦関数と、正弦の 4 ピース多項式モデルのどちらが速いと思いますか?


 auto y = std::sin(x); // vs. y = -0.000182690409228785*x*x*x*x*x*x*x +0.00830460224186793*x*x*x*x*x -0.166651012143690*x*x*x +x;


次の問題。ショートサーキットで論理演算を使用するか、コンパイラをだましてそれを回避し、論理式を一括で計算することで、どちらが高速に動作しますか?


 if (xs[i] == 1 && xs[i+1] == 1 && xs[i+2] == 1 && xs[i+3] == 1) // xs are bools stored as ints // vs. inline int sq(int x) { return x*x; } if(sq(xs[i] - 1) + sq(xs[i+1] - 1) + sq(xs[i+2] - 1) + sq(xs[i+3] - 1) == 0)


そしてもう1つ。トリプレットをより速くソートするのはどれですか?スワップソートとインデックスソート?


 if(s[0] > s[1]) swap(s[0], s[1]); if(s[1] > s[2]) swap(s[1], s[2]); if(s[0] > s[1]) swap(s[0], s[1]); // vs. const auto a = s[0]; const auto b = s[1]; const auto c = s[2]; s[int(a > b) + int(a > c)] = a; s[int(b >= a) + int(b > c)] = b; s[int(c >= a) + int(c >= b)] = c;


すべての質問に、考えたりグーグルで検索したりせずに断固として答えた場合、あなたの直感はあなたを裏切りました。あなたは罠を見ませんでした。これらの質問には、文脈がなければ明確な答えはありません。


コードはどの CPU または GPU をターゲットにしていますか?コードをビルドするのはどのコンパイラですか?どのコンパイラ最適化がオンで、どれがオフですか?そのすべてを把握して初めて予測を開始できます。さらに良いのは、特定のソリューションごとに実行時間を測定することです。


  1. -O2 -march=nativeを指定して clang 11 でビルドし、 Intel Core i7-9700Fで実行した場合、多項式モデルは標準のサインよりも3 倍高速です。しかし、 --use-fast-mathを指定して nvcc を使用し、GPU、つまりGeForce GTX 1050 Ti Mobileで構築した場合、標準正弦はモデルよりも 10 倍高速です。


  2. ベクトル化された算術演算のために短絡回路ロジックを交換することは、i7 でも理にかなっています。スニペットの動作が 2 倍速くなります。ただし、clang と -O2 が同じ ARMv7 では、標準ロジックはマイクロ最適化よりも25% 高速です。


  3. また、インデックス ソートとスワップ ソートの比較では、インデックス ソートは Intel で 3 倍、スワップ ソートは GeForce で3 倍高速です。


したがって、私たちが大好きなマイクロ最適化は、コードを 3 倍高速化し、90% 遅くする可能性があります。それはすべて文脈に依存します。ビルド ターゲットを切り替えると、インデックス ソートが奇跡的にスワップ ソートに変わるなど、コンパイラが最適な代替案を選択できたらどんなに素晴らしいことでしょう。しかし、それはできませんでした。


  1. コンパイラがサインを多項式モデルとして再実装することを許可したとしても、速度と引き換えに精度を上げても、ターゲットの精度はまだわかりません。 C++ では、「この関数はそのエラーを許容する」とは言えません。私たちが持っているのは、「--use-fast-math」のようなコンパイラ フラグだけで、翻訳単位の範囲内にあるだけです。


  2. 2 番目の例では、コンパイラは、値が 0 または 1 に制限されていることを認識しておらず、可能な最適化を提案できない可能性があります。適切な bool 型を使用することでおそらくそれをほのめかすこともできたでしょうが、それはまったく別の問題だったでしょう。


  3. そして 3 番目の例では、コードの断片が大きく異なっているため、同義語として認識されます。コードを詳しく説明しすぎました。それが単に std::sort である場合、これにより、コンパイラーはアルゴリズムを選択する自由度が高くなります。しかし、どちらも大規模な配列では非効率的であり、std::sort は一般的な反復可能なコンテナーで動作するため、スワップ ソートではなくインデックス ソートを選択することはありませんでした。


そして、それがスパイラルにたどり着く方法です。これは、カーネギー メロン大学とチューリッヒ工科大学の共同プロジェクトです。 TL&DR: 信号処理の専門家は、新しいハードウェアごとにお気に入りのアルゴリズムを手動で書き直すことに飽き飽きし、この作業を行うプログラムを作成しました。プログラムは、アルゴリズムの高レベルな記述とハードウェア アーキテクチャの詳細な記述を取得し、指定されたハードウェアに対して最も効率的なアルゴリズムの実装になるまでコードを最適化します。


Fortran と同様の重要な違いである Spiral は、数学的な意味で最適化問題を実際に解決します。ランタイムをターゲット関数として定義し、ハードウェア アーキテクチャによって制限される実装バリアントの因子空間でグローバル最適を探します。これは、コンパイラが実際に行うことはありません。


コンパイラは真の最適解を探しません。プログラマーによって教えられたヒューリスティックに導かれたコードを最適化します。基本的に、コンパイラは最適解を探す機械としてではなく、アセンブリ プログラマとして機能します。優れたコンパイラは優れたアセンブリ プログラマのように機能しますが、それだけです。




スパイラルは研究プロジェクトです。範囲と予算に限りがあります。しかし、それが示す結果はすでに印象的です。高速フーリエ変換では、彼らのソリューションは MKL と FFTW の両方の実装を決定的に上回っています。彼らのコードは最大 2 倍高速です。インテルでも。


達成の規模を強調するために、MKL は Intel 自身による Math Kernel Library であり、ハードウェアの使い方を最もよく知っている人たちによるものです。そして、WWTF 別名「西部で最速のフーリエ変換」は、アルゴリズムを最もよく知っている人たちによる高度に専門化されたライブラリです。彼らはどちらもチャンピオンであり、Spiral が 2 人を 2 倍上回っているという事実は驚くべきことです。


Spiral が使用する最適化技術が完成して製品化されると、C++ だけでなく、Rust、Julia、さらには Fortran でさえ、これまでに直面したことのない競争に直面することになります。高レベルのアルゴリズム記述言語で書くとコードが 2 倍速くなるのに、なぜ C++ で書くのでしょうか?


C++ キラーその 2。Numba

最高のプログラミング言語は、あなたがすでによく知っている言語です。数十年連続で、ほとんどのプログラマーにとって最もよく知られている言語は C でした。また、他の C 系言語がトップ 10 に密集しており、TIOBE インデックスをリードしています。しかし、わずか 2 年前に前代未聞のことが起こりました。 C は他の何かに最初の場所を与えました。


「何か他のもの」は Python のように見えました。この言語は、90 年代には誰も真剣に受け止めませんでした。それは、私たちがすでにたくさん持っていたもう 1 つのスクリプト言語だったからです。



誰かが「ああ、Python は遅い」と言うでしょう。これはナンセンスな用語なので、ばかげているように見えます。アコーディオンやフライパンと同じように、言語も速くも遅くもなりません。アコーディオンの速度が演奏者に依存するように、言語の「速度」はコンパイラの速度に依存します。


「しかし、Python はコンパイル済み言語ではありません」と誰かが続けて、また失敗するかもしれません。 Python コンパイラはたくさんありますが、その中で最も有望なのは Python スクリプトです。説明させてください。


私はかつてプロジェクトを持っていました。もともと Python で書かれ、「パフォーマンスのために」C++ で書き直され、GPU に移植された 3D プリント シミュレーションで、私が入社する前にすべての作業が行われました。その後、数か月かけてビルドを Linux に移植し、GPU コードを最適化しました。 Tesla M60 は、その時点で AWS で最も安価であったため、Python の元のコードに合わせて C++/CU コードのすべての変更を検証しました。だから私は、幾何学的アルゴリズムを考案するという、私が通常専門とすること以外はすべてやりました。


最終的にすべてが機能するようになったとき、ブレーメンのアルバイトの学生から電話があり、「あなたは異種混合が得意なので、GPU で 1 つのアルゴリズムを実行するのを手伝ってくれませんか?」と尋ねられました。もちろん!私は彼に CUDA、CMake、Linux のビルド、テスト、最適化について話しました。たぶん1時間話しました。彼はそのすべてを非常に丁寧に聞いていましたが、最後にこう言いました。関数があり、その定義の前に @cuda.jit を書きました。Python は配列について何かを言い、カーネルをコンパイルしません。ここで何が問題になるか知っていますか?」


知りませんでした。彼は一日でそれを自分で理解しました。どうやら、Numba はネイティブの Python リストでは動作せず、NumPy 配列のデータのみを受け入れます。そこで彼はそれを理解し、GPU でアルゴリズムを実行しました。パイソンで。彼には、私が何ヶ月も費やした問題はありませんでした。 Linuxで使いたいですか?問題ありません。Linux で実行するだけです。 Python コードと一貫性を持たせたいですか?問題ありません。これは Python コードです。ターゲット プラットフォーム用に最適化しますか?また問題ありません。 Numba は、コードを実行するプラットフォームに合わせてコードを最適化します。これは、コードが事前にコンパイルされるのではなく、デプロイ済みのときにオンデマンドでコンパイルされるためです。


それは素晴らしいことではありませんか?うーん、ダメ。とにかく私のためではありません。 Numba では決して起こらない問題を C++ で解決するのに何ヶ月も費やし、ブレーメンのパートタイマーが数日で同じことをしました。初めての Numba 体験でなければ、数時間かかっていたかもしれません。では、このナンバーは何ですか?それはどんな魔術ですか?


魔術なし。 Python デコレータは、コードのすべての部分を抽象構文ツリーに変換するので、それを使って好きなことを行うことができます。 Numba は、任意のバックエンドとサポートする任意のプラットフォームで抽象構文ツリーをコンパイルする Python ライブラリです。 Python コードをコンパイルして CPU コア上で大規模な並列処理を実行する場合は、Numba にそのようにコンパイルするように指示するだけです。 GPU で何かを実行したい場合は、もう一度尋ねてください


 @cuda.jit def matmul(A, B, C): """Perform square matrix multiplication of C = A * B.""" i, j = cuda.grid(2) if i < C.shape[0] and j < C.shape[1]: tmp = 0. for k in range(A.shape[1]): tmp += A[i, k] * B[k, j] C[i, j] = tmp


Numba は、C++ を時代遅れにする Python コンパイラの 1 つです。ただし、理論的には、同じバックエンドを使用するため、C++ よりも優れているわけではありません。 GPU プログラミングには CUDA を使用し、CPU には LLVM を使用します。実際には、新しいアーキテクチャごとに事前に再構築する必要がないため、Numba ソリューションはすべての新しいハードウェアとその利用可能な最適化によりよく適応します。


もちろん、Spiral のように明確なパフォーマンス上の利点がある方がよいでしょう。しかし、Spiral はどちらかというと研究プロジェクトであり、C++ を殺すかもしれませんが、最終的には運が良ければです。 Numba with Python は現在、リアルタイムで C++ を絞め殺しています。 Python で記述でき、C++ のパフォーマンスを備えている場合、なぜ C++ で記述したいのでしょうか?


C++ キラー 3. ForwardCom

別のゲームをしましょう。 3 つのコードを示します。そのうちの 1 つ、またはそれ以上がアセンブリで記述されていることがわかります。どうぞ:


 invoke RegisterClassEx, addr wc ; register our window class invoke CreateWindowEx,NULL, ADDR ClassName, ADDR AppName,\ WS_OVERLAPPEDWINDOW,\ CW_USEDEFAULT, CW_USEDEFAULT,\ CW_USEDEFAULT, CW_USEDEFAULT,\ NULL, NULL, hInst, NULL mov hwnd,eax invoke ShowWindow, hwnd,CmdShow ; display our window on desktop invoke UpdateWindow, hwnd ; refresh the client area .while TRUE ; Enter message loop invoke GetMessage, ADDR msg,NULL,0,0 .break .if (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .endw


 (module (func $add (param $lhs i32) (param $rhs i32) (result i32) get_local $lhs get_local $rhs i32.add) (export "add" (func $add)))


 v0 = my_vector // we want the horizontal sum of this int64 r0 = get_len ( v0 ) int64 r0 = round_u2 ( r0 ) float v0 = set_len ( r0 , v0 ) while ( uint64 r0 > 4) { uint64 r0 >>= 1 float v1 = shift_reduce ( r0 , v0 ) float v0 = v1 + v0 }


では、どの 1 つまたは複数がアセンブリに含まれているのでしょうか? 3つともそう思った方、おめでとうございます!あなたの直感はすでにはるかに良くなっています!


最初のものは MASM32 にあります。これは、「if」と「while」を使用してネイティブ Windows アプリケーションを作成するマクロアセンブラーです。そうです、「以前は書いていた」のではなく、今でも「書く」のです。 Microsoft は、Windows の Win32 API との下位互換性を熱心に保護しているため、これまでに作成されたすべての MASM32 プログラムは最新の PC でも適切に動作します。


皮肉なことに、C は PDP-7 から PDP-11 への UNIX 変換を容易にするために発明されました。これは、70 年代のハードウェア アーキテクチャのカンブリア紀の爆発に耐えられるポータブル アセンブラとして設計されました。しかし、21 世紀にはハードウェア アーキテクチャの進化が非常に遅いため、20 年前に MASM32 で書いたプログラムは、今日では完全に組み立てられて実行されますが、昨年 CMake 3.21 で作成した C++ アプリケーションが今日 CMake で作成されるとは確信が持てません。 3.25.


2 番目のコードは Web Assembly です。これはマクロ アセンブラでもなく、"if" や "while" がなく、ブラウザで人間が読めるマシン コードに近いものです。または他のブラウザ。概念的には、任意のブラウザー。


Web アセンブリ コードは、ハードウェア アーキテクチャにまったく依存しません。それが提供するマシンは、抽象的、仮想的、普遍的であり、好きなように呼んでください。このテキストが読める場合は、物理マシンにすでに 1 つ入っています。


しかし、最も興味深いコードは 3 番目のコードです。それは、C++ およびアセンブリ最適化マニュアルの有名な著者である Agner Fog が提案するアセンブラである ForwardCom です。 Web Assembly と同様に、この命題はアセンブラよりも、後方互換性だけでなく前方互換性を可能にするように設計された汎用命令セットを対象としています。したがって、名前。 ForwardCom の正式名称は、「オープンな前方互換性のある命令セット アーキテクチャ」です。言い換えれば、これは集会の提案ではなく、平和条約の提案です。


最も一般的なアーキテクチャ ファミリ (x64、ARM、および RISC-V) にはすべて、異なる命令セットがあることがわかっています。しかし、このままにしておく正当な理由は誰にもわかりません。おそらく最も単純なものを除いて、すべての最新のプロセッサは、入力されたコードではなく、入力を変換するマイクロコードを実行します。したがって、Intel の下位互換性レイヤーを備えているのは M1 だけではなく、すべてのプロセッサが基本的に、それ以前のすべてのバージョンに対する下位互換性レイヤーを備えています。


では、アーキテクチャ設計者が同様のレイヤーに同意することを妨げているのは、前方互換性のためでしょうか?直接の競争にある企業の相反する野望を除けば、何もありません。しかし、もしプロセッサメーカーが他のすべての競合他社ごとに新しい互換性レイヤーを実装するのではなく、ある時点で共通の命令セットを持つことに落ち着いたら、ForwardCom はアセンブリプログラミングをメインストリームに戻すでしょう.この前方互換性レイヤーは、あらゆるアセンブリ プログラマーの神経症を癒してくれます。


前方互換性レイヤーにより、それ自体が陳腐化することはありません。それがポイントです。


アセンブリ プログラミングは、アセンブリでの記述は難しく、非現実的であるという神話によっても妨げられています。 Fog の命題は、この問題にも対処しています。アセンブリで書くのは難しく、C で書くのは難しくないと思う人がいたら、アセンブラを C のように見せてみましょう。問題ありません。現代のアセンブリ言語が、1950 年代の祖父の外観とまったく同じように見える正当な理由はありません。


3 つのアセンブリ サンプルを自分で見ました。それらのどれも「従来の」アセンブリのようには見えませんし、そうであってはなりません。


したがって、ForwardCom は、決して時代遅れにならない最適なコードを記述できるアセンブリであり、「従来の」アセンブリを習得する必要はありません。すべての実用的な考慮事項について、これは未来の C です。 C++ ではありません。

では、С++ はいつ死ぬのでしょうか?

私たちはポストモダンの世界に住んでいます。人以外に死ぬものはもうありません。 COBOL、Algol 68、および Ada のように、ラテン語が実際に死ななかったのと同じように、C++ は、生と死の間の永遠の半分存在する運命にあります。 C++ が実際に死ぬことはありません。新しい強力なテクノロジによって主流から追い出されるだけです。


まぁ、「押される」じゃなくて「押される」。私は C++ プログラマーとして現在の仕事に就きました。今日、私の仕事は Python から始まります。私が方程式を書き、SymPy がそれらを解いてから、解を C++ に変換します。次に、このコードを C++ ライブラリに貼り付けます。形式を少し変更する必要さえありません。静的アナライザーは名前空間を台無しにしていないことをチェックし、動的アナライザーはメモリ リークをチェックします。 CI/CD は、クロスプラットフォームのコンパイルを処理します。プロファイラーは、自分のコードが実際にどのように機能するかを理解するのに役立ち、逆アセンブラーはその理由を理解するのに役立ちます。


C++ を「C++ ではない」と交換しても、作業の 80% はまったく同じままです。 C++ は、私がしていることのほとんどとは無関係です。私にとって、C++ はすでに 80% 死んでいるということでしょうか?



安定拡散によるリードイメージ。