HN 日本語サマリー

← 一覧へ戻る
プログラミング

整数型に関する考察 (2023年)

Thoughts on Integers (2023) (blog.xoria.org)

10 pointsby mpweiher2 コメント

要約

この記事は、プログラミング言語における整数型の扱いについて、C、C#、Go、Swift、Rustなどの例を比較しながら論じています。特にRustの明示的で偏りのない整数型設計を高く評価し、C言語における符号付き整数オーバーフローの未定義動作(UB)がパフォーマンスに与える影響についても考察しています。

全文翻訳

この記事では、整数型に関する私の見解と、私がどのようにしてそれらの見解に至ったかを説明します。うまくいけば、最初は同意できず、最後には納得していただけるでしょう! 現状 まず、今日の一般的なプログラミング言語における現状を特徴づけます。ただし、いくつか注意点があります。この文章では、明白な理由からスクリプト言語については触れません。また、Javaは整数型(intとInteger、および明確な符号なし型がない)を巡る奇妙な点があるため、このセクションでは除外します。 古典的なシステムプログラミング言語であるCから始めましょう。従来のCの整数型(int、char、short、long、long longなど)に加えて、C99はint8_t、uint64_tなどの名前を持つ固定サイズの型を追加しました。最新の64ビットプラットフォームでは、intは常に符号付きで32ビット幅です。さらに、Cにはptrdiff_tやsize_tのような、マシン依存の紛らわしい型があります。一般に、添え字とサイズはsize_t型ですが、Cの暗黙的な型変換により、intやその他の型が代わりに使われることがよくあります。Cで符号付き整数がオーバーフローすると、未定義の動作(undefined behavior)が発生します。これは、コンパイラがオーバーフローが決して発生しないと仮定することを意味します。一方、符号なし整数のオーバーフローは、ラップアラウンド(wrap)するように定義されています。 次に、C#です。これは、低レベルの制御が必要ない場合に使用される、典型的な「アプリケーションプログラミング」言語と特徴づけられます。C#の型名はCに似ています:int、short、long、byte。intは32ビット幅です。符号なしバージョンは、型名の前にuを付けることで取得できます。添え字とサイズは通常のintを使用し、デフォルトでは整数はオーバーフロー時にラップします。 Goは、C#と同じ領域の言語の現代的な代替手段です。ここでトレンドの始まりが見られます。直感的でないCスタイルの整数型名を使用する代わりに、Goは各型のビット幅を付加するだけです:int8、uint64など。もう1つの興味深い違いは、intとuintのサイズにあります。これらは特定の幅にハードコードされるのではなく、ターゲットプラットフォームのアドレスサイズに一致します。ここでも、サイズと添え字はintを使用します。Goは整数オーバーフローをラップすると定義しています。 Swiftは、この「コンパイル済み、静的型付け、非低レベル商用ソフトウェア」ニッチに属するもう1つの新しい言語であり、その整数型はGoといくつかの点で似ています。型名は大文字小文字を除いてGoと同じです:Int8、UInt64など。IntとUIntはGoと同じサイズ設定の動作を持ち、添え字とサイズは両方ともIntを使用します。奇妙なことに、Swiftはリリースビルドでも、整数オーバーフロー時にデフォルトでパニック(panic)します。 Rust:より良いアプローチ 上記でリストしたすべての例に共通点がありましたか?それらの言語すべてに、何らかのデフォルトのint型があります。符号付き型ではなく符号なし型を使用したり、デフォルトサイズのintではなく明示的にサイズ指定された型を使用したりするには、追加のタイピングが必要です。これはintの使用を他の型よりも促進すると言えます。結局のところ、画面いっぱいのintとuint32_tのどちらを見たいですか?これは見た目以上の問題です。単にintと入力する怠惰な方法を取ることは非常に簡単であり、その仕事に最適な型を特定するためにユースケースを精査するのではなく、考えなしに行われます。 Rustはこれらのすべての点を排除します。すべての整数型には明示的なサイズがあり、すべての型はそれぞれ符号付きと符号なしを示すiまたはuでプレフィックスが付けられています。特定のサイズや符号付き/なしに対する偏見はありません。状況を考慮し、その状況に最も適した型を判断することが強制されます。整数が狭められるとメモリが節約され、バグは防止されます。Rustコミュニティでしばしば引用されるドグマによれば、無効な値(負の数)は表現不可能になります。 名前 | 符号付き? | ビット単位のサイズ u8 | いいえ | 8 u16 | いいえ | 16 u32 | いいえ | 32 u64 | いいえ | 64 usize | いいえ | アドレスサイズ i8 | はい | 8 i16 | はい | 16 i32 | はい | 32 i64 | はい | 64 isize | はい | アドレスサイズ この表で気づくかもしれないことの1つは、usizeとisizeの存在です。usizeは、Cのsize_tのように、添え字とサイズに使用されます。Cとは異なり、Rustには暗黙的な整数キャストがないため、配列インデックスには本当にusizeを使用する必要があります(asキャストの海に溺れたくない場合)。32ビットintではなくusizeを使用することは、配列が20億を超える要素を持つことができることを意味し、符号なし型を使用することは、負の配列インデックスが決して発生しないことを意味します。 Rustはデバッグビルドではバグを検出するためにオーバーフロー時にパニックしますが、パフォーマンスのためにリリースビルドではラップを使用します。オーバーフロー時のUBの欠如に注意してください。 Rustが特定の整数型に偏っていないという私の発言には注意点があります。 fn demo() { let x = 1; // x: i32 } Rustは整数リテラルの型情報がない場合、デフォルトでi32になるため、(マイナーではありますが)符号付き32ビット整数への偏りがあります。しかし、Rustの経験から、符号付き整数を非常にまれに使用することに気づいたため、符号なし整数をデフォルトにすることをほとんど主張するでしょう。もしRustが符号なし整数と符号付き整数を平等に扱っていなければ、私はこの結論に決して達しなかったでしょう。 これらの変更はすべて明白な改善のように思えます。 Rustプログラマーが他の言語を試す C#、Swift、Goを試し始めたとき、私はすぐにフラストレーションを感じました。タスクに最適な整数型を選択するという私の姿勢で、これらの言語がデフォルトで使う符号付きintの代わりに、配列インデックスにuintを everywhere 使用しようとしました。必要なキャストの量が多いため非現実的であり、フラストレーションから諦めました。これは私をさらに確信させました。なぜすべての言語がRustのような整数型を持てないのでしょうか? そもそもCはなぜ符号付き整数オーバーフローをUBにするのか? 当初の理由はハードウェアの動作の違いでしたが、今日最もよく聞かれる議論はパフォーマンスです。しかし、ほとんどのCPUは算術演算がオーバーフローしたときにラップ処理を行うのではないでしょうか?なぜオーバーフローをラップするように定義しないのですか(CPUが「無料」で提供するため、追加コストなしで)そしてそれで終わりにするのですか? 私はChandler CarruthのUBに関する講演を見ましたが、その中で彼は啓発的な例を含んでいます。(64ビットアドレスのアーキテクチャで実行されていると仮定します。 1) bool mainGtU(int32_t i1, int32_t i2, uint8_t *block) { uint8_t c1, c2; c1 = block[i1]; c2 = block[i2]; if (c1 != c2) return c1 > c2; i1++; i2++; c1 = block[i1]; c2 = block[i2]; if (c1 != c2) return c1 > c2; i1++; i2++; // さらに数回繰り返します } コンパイラは、i1とi2の変更を削除するのに十分賢いです。バイトはblock + i1とblock + i2から直接ロードされ、次にblock + i1 + 1とblock + i2 + 1、そしてblock + i1 + 2とblock + i2 + 2のようにロードされます。これらのアドレス計算はロード命令自体の一部として行われるため、64ビット整数の範囲内で行われます。 ラップを強制した場合を考えてみましょう。i1++;とi2++;は、32ビット整数の限界に達するとラップします。ランタイム動作がこのラップに忠実であることを保証するために、コンパイラは、i1 + nが32ビットをオーバーフローしたときにblock + i1 + nがblock + 0にラップバックするようにする追加の命令を生成します。言い換えれば、「CPUはオーバーフロー時に無料でラップする」というのはこのケースでは当てはまらず、結果としてコードが肥大化します。 これらはすべて仮説ではありません。i1とi2を符号なしにすると、これらの追加命令も発行されます。彼の講演で、Chandlerはこのコードを、符号なし整数から符号付き整数に切り替えることでパフォーマンスが向上する例として使用しています。なぜなら、それはコンパイラがオーバーフロー時のUBを利用できるようにするからです。彼は次のように述べています。 算術的に、またはモジュラー空間(2のべき乗)で扱いたい整数がある場合は、それを符号付きにしてください。より多くのビットが必要な場合は、より多くのビットを取得してください。それを符号付きのままにしてください。 これは私にとって驚くべきことでした。バグを防ぐために型システムを使用するべきではありませんか?数値が負になることが決してない場合、なぜそれを符号付きにするのでしょうか?この問題に関するChandlerの視点は私を不快にさせました。 Cへのさらなる探求 Cを使ったシステムプログラミングに没頭するにつれて、負になることはないにもかかわらず、インデックスには符号付き整数を使用すべきだと主張する人々に数多く出会いました。特に、Chris Wellonsのブログでこの見解を支持する3つの異なる投稿を読みました。 近年、私は確信させられていま