前々回の話題の途中に出てきた 補数表現
まわりについて、自分の知識が及んでいない感じがしたのを受けて今回はそのおさらいをしながら、学びきれていない事柄を見つけて理解を深めてみようと思います。それが終わったら再び本編に戻って 強参照循環
を解消する手段のうちのもうひとつ、弱参照
まわりの挙動を具体的に眺めて行ってみますね。よろしくお願いします。
————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #235
00:00 開始 00:22 今回は補数表現の話 01:07 補数の性質 02:50 整数の二進数表現 06:29 bitPattern は Double の機能 08:30 符号も特別視しないビット表現の確認方法 10:06 文字列の頭をゼロフィルする 11:59 演算したときのビットの変化 16:51 2の補数表現による減算 18:00 補数表現とは 21:55 加算演算で減算が可能 23:28 補数表現に関するその他 24:31 1の補数表現 25:11 論理否定演算子 26:23 補数とは? 29:25 クロージングと次回の展望 —————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #235
さて、勉強会を始めましょう。今回は個数表現の話についてですね。前々回の勉強会で、二進数に関連する話をしました。その中で、UInt
(Unsigned Integer)についての質問や感想があったので、それに答える形で今日の内容を進めていきます。
プログラミングの分野では、足し算の演算さえできれば、自動的に引き算も実現できるのです。これは二進数の世界でも同じで、足し算の演算によって引き算が可能になります。たとえば、10 - 5
を10 + (-5)
と見なすことができるわけです。
これを実際に書いて確認してみましょう。例えば、10 - 5
という計算は、10 + (-5)
と同じように足し算の演算で引き算を表現できます。これを二進数でも表現してみましょう。
二進数の世界では、複合付き演算をシンプルにビット操作できるようになっています。ここで、Swiftのプレイグラウンドなどを使って具体的に見ていくと理解しやすくなると思います。ですので、iOS用のプレイグラウンドを使ってみましょう。
例えば、十進数で1
と-1
があったとき、直感的に十進数の「1に対するマイナスは-1」であるとわかりますね。同じ表現のまま、二進数で扱ってみましょう。-1
を二進数に変換する場合、ビットパターンが少し違います。
具体的に見てみます。-1
のビットパターンを確認すると、それは全てのビットが1
になります。これが2の補数表現です。実際にビットパターンとして確認してみましょう。例えば、8ビットで表現した場合、11111111
となります。これは-1
を表現しています。
Swiftでは、整数のキャストを使ってビットパターンを確認できます。例えば、let a: Int8 = -1
とすると、ビットパターンは11111111
になります。そしてこれを UInt8
にキャストすると、ビットの表示は変わりませんが、値が変わるわけです。
要は、-1
をUInt8
として見ると、255
になります。つまり、8ビットの11111111
は255
(十進数)に相当します。
このように、負の数も二進数の世界ではビット操作によって表現しなおすことができるという内容です。
非常に興味深い話ではないでしょうか。これを理解すると、プログラミングでの数値演算がより直感的に感じられるようになりますね。どうでしょうか、このように足し算と引き算の関係やビットパターンについて理解を深めてきました。次に具体的なコード例を見て実装していきましょう。 ビットパターンがどう扱われるかについて議論しています。ビットパターンを扱う際には特定の型に依存することがあるため、不動小数点数(浮動小数点数とも言います)のビットパターンが問題になることがあります。例えば、UInt64
型のビットパターンを確認しようとすると、これが浮動小数点数(特にDouble
型)のビットパターンとして解釈される場合があります。
浮動小数点数に関するビット表現は複雑で、特に仮数部(significand)や指数部(exponent)の扱いが重要です。仮数部が全部ゼロの場合は特別な値として解釈されることがあります。このような背景を踏まえ、先ほどのビットパターンが出てきた際には、浮動小数点数としてのビット表現になっていたために期待とは違う結果になったと言えます。
適切にビットパターンを確認するには、例えば次のようなコードを使って、UInt
型のビットパターンを文字列の二進表現に変換して確認するのが良いでしょう。
let a: UInt = 5
let b: UInt = 3
print(String(a, radix: 2)) // 二進数表現
print(String(b, radix: 2))
このコードでは、変数a
およびb
を二進数表現で表示しています。また、変数をビット単位で操作する場合は、特定のビット数(例えば16ビット)に収めたいことがあるかもしれません。そのため、以下のようにUInt16
に変換すると良いでしょう。
let number: Int16 = -3
let bitPattern = String(UInt16(bitPattern: number), radix: 2)
print(bitPattern) // ビットパターンを表示
このコードはInt16
型の変数をUInt16
型に変換して、そのビットパターンを二進数表現で表示します。これにより、符号付き数(Int16
)を符号なし数(UInt16
)として扱うことが可能になります。
さて、チャットGPTの結果やAIの話に少し触れましたが、AIが生成するコードには様々なソースから情報が引っ張られています。そのため、結果が予想外の形になることもあります。
最後に、ゼロ埋め(ゼロフィル)したい場合は、文字列のフォーマッターやライブラリを使用することが考えられます。Swiftでは、例えば以下のようにしてゼロ埋めを実現できます。
let value = 5
let zeroFilledValue = String(format: "%016b", value)
print(zeroFilledValue) // 16ビットのゼロ埋め二進数表現
このコードは、value
を16ビットのゼロ埋め二進数表現に変換して表示します。
このように、目的に応じて正しい型変換や表示形式を使うことが重要です。 うちのブレイクラウンドとは何だろう。何かオーバーフローだったらそうですよね。そうですね、ビットパターンの変換が間違っているのかな。これ普通にビット演算ですね。使ってる足し算するから、基本的にはオーバーフローする演算なんですね。レジスタではオーバーフローしたら最上位ビットにしてくるから、普通に演算するんだけど、プログラミング言語で書くと、ひょっとして例外が発生してオーバーフローエラーになるような気もしますが、どうでしょうね。
今の話を聞いて分かった気がしたけど、やっぱり分かってなかったですね。閃いた気がしたんですが、実際は閃いてなかったです。どんどん分かっていくんですかね。
ちょっとCとDをまず表示してみますか。数表現意見で、ちょっと何か言ってますね。&-3
でしょう。これをビットパターンで変換しちゃってるから、もう32ビットに変えようと16のままでも同じことをやってて、今日はこれとこれを指してるようなイメージ。それの32ビット版をやってて、そうするとヘタ溢れるじゃないですか。それで落ちてるんだ。分かった、分かりました。だからここでオーバーフローで落ちてるんですね。
なるほど、オーバーフローまわりの処理も重要ですね。つまり、こうやってAとBを足すときにオーバーフローを無視する演算をしてあげると0になるよ、これをやりたかったんです。ちょっと演算子を使っちゃうと、何をやっているんだか分からなくなるでしょうけど、とにかくこれが2の補数表現ですよね、確かね。これとこれを足し合わせるとオーバーフローするから、0のようですが1個溢れた1を捨てている、そういった状況になるように作られている。だからこれが13
と-13
でもそうなんですよ。ビットパターンをうまく調整して足したときに0になる。要は引いてるってことですよ、13引く13が足し算演算で実現できてるっていうね。こういった工夫が補数表現だったはず。
という前置きはこのくらいにして、補数表現を調べてみましょう。Wikipediaが好きなので、ちょっとWikipediaの方を直接見ていきますけれど、どうしましょうか。2の補数を先に見るか、この前ね1の補数と2の補数、これ何が違うんだっけみたいな話がありましたね。ここからね、補数が気になったけど、まず2の補数を見てみましょう。
捨てるとはここでは書かれてないので、捨てることは応用なのかな。2を基数とした場合の補数である。また、元の数の補数と呼ばれている。ここが知りたいですね、個人的に。補数と呼ばれるものには基数の補数と減基数の補数、だから2の補数と1の補数があるってことが二進数ではね。特定の基数2に対しては、それぞれ2の補数、1の補数と呼ばれる。間違ってたら読み進めてるうちに判明すると思うので、とりあえず読み進めます。
n進数に対する基数の補数とnのプラス1進数に対する減基数の補数は、いずれもnの補数と呼ばれる要素点。なんとなくそうなんだなと思って読み進めますが、当たり前そうな事柄に対してちゃんと要素点って書いてあるのは素晴らしいですね。
とりあえず、今まで聞いたことのない言葉ばかりだからね。まぁいいや、とりあえずそれっぽいと捉えておいて。特に断りがない限り、ここでは2の補数は二進数に対する基数の補数であって、三進数に対する減基数の補数ではない、そうですよね。混乱してきますよね、先のこの選択中の内容を読むとね。
まぁ、とりあえず進めます。整数xとの和が2のべき乗となるxの補数。2のn乗マイナスxのことを言う。シンプルな式だけど理解できなかったぞ。2のn乗マイナスx。整数xとの和が2のべき乗、ああ、そういう事か。オーバーフローするような数字から、その対称となる数字を引いたやつですね。なるほど、だからマイナス13だとすると、ああ、またn
を出さないといけないのか、とにかくそういう式で表現される。理屈的な話。
基数の補数という概念があれば、それの二進数で、先ほど見た通り足し合わせると最上位の桁だけが1になってそれ以外は全部0になるっていうのが、2の補数。いいですね。そういう二進数だった時にそれに対する2の補数としてはこういう表現になって、これが出てきたのはね、足し合わせると1桁上がるような数字ね。この数字が補数なんですね。いいですね。 プレイグラウンドより出力がずっと綺麗でわかりやすいですね。8ビットが9ビットになるけど、8ビットに丸め込むことで0になります。そうすることで、加算減算が簡単にできるという利点があります。
固定長整数型で使われると伸びましたよね。固定小数点数でも使われるんだそうです。固定小数点数はあまりメジャーじゃないのであまり馴染みがないかもしれませんが、とりあえずそんな感じです。
2の個数についても話をしましたが、この辺はコンピュータが自動的にやってくれるので、あまり気にする必要はありません。アルゴリズムもありますが、その詳細はあまり深入りしなくても良いでしょう。2の個数についてはよくわかりました。
次に、1の個数表現についても少し触れました。そこで勉強会などを少し調べてみましたが、取り上げられない最小の数については、特に詳しい結論は出ていませんでした。
Swiftでは特定の演算がどう表現されているのかという話もありました。例えば、「int x = 8; int y = x << 1
」のような演算を説明しましたが、これはすべてのビットが1になるということです。この場合、オーバーフローは起こらないため、安心して使えます。
そもそもの個数が何かについても触れました。個数はあるxとの和が基準となる数Cに等しくなるような数であり、x + x = C
という形で表されます。このCは2進法の基数のべきで表されます。例えば、2進法で2のn乗となると、このnはビット数を指します。なので、16進数だと16ビットの数になります。
次回は共産省の順番に関する話に進む予定です。今日はここまでといたします。お疲れ様でした。ありがとうございました。