先日、このBlogを書くための調べごとの中で気づいたことがあって、
それはC言語で符号付き整数の演算でオーバーフロー時の挙動は未定義ということである。
えっ、そうだったの? それだと仕事で作ってるシステムで問題じゃないかと。
未定義というのは、コンパイラやプロセッサの都合のいいように決めてよいということである。
int16_tの場合、通常は最大値は32767、最小値は-32768である。
int16_t型の変数xが32767のとき、x++;がどうなるかは不明である。
この処理単独ならば、ほぼ間違えなく-32768になるのですが。
ただ、他の処理と合わせた最適化の中でどうなるかはわからない。
一方で符号無し整数型のオーバーフロー時の挙動は明確である。
uint16_tの場合、通常は0~65535の値を表すことができるが、
この範囲を超える場合は65536で割った余りとなると規定されている。
すなわちuint16_t型の変数yが65535のとき、y++;をすると0になる。
こういう動作をラップアラウンドって言ってますね。
ちなみにこの考え方は負の数を代入するときにも適用され、
-64をuint16_t型にキャストすると -64+65536=65472 となる。
これは負の数は2の補数で表現されると言っているのと同じである。
逆に符号なし整数を符号付き整数に変換する場合の挙動だが、
符号付き整数型の範囲外の値をどうするかは、処理系定義となっている。
処理系定義ってのはコンパイラやプロセッサの都合のいいように決めて良いということだが、
さっきも未定義の挙動と違って、マニュアルで定義されているということである。
とはいえ、ほとんどの場合はビットパターンをそのまま転記する。
そして最上位ビットが1の場合は2の補数で表された負の数として扱われる。
符号無しの 65472u をint16_t型に変換すると -64 となるわけである。
なので冒頭に「int16_t型の変数xが32767のとき、x++;がどうなるかは不明」と書いたが、
x = (int16_t)( ((uint16_t)x) + 1u); だと x=32767のとき-32768、x=-1のとき0となるとほぼ断言できる。
符号無しで 32767+1=32768 と演算して、これをint16_t型に変換すると、
典型的な処理系定義の動作ではこれを2の補数で表された負の数と解釈し、-32768 となる。
負の数をuint16_tに変換するというのは65536を加えた値にすることで、
int16_t型の-1はuint16_tにキャストすると 65565となる。これに1加えるとラップアラウンドして0となる。
0は符号付き型に変換しても当然0なので、これは処理系定義の動作も関係ない。
負の数がオーバーフローしたときの動作はわからないのに、
符号付き・符号無しとも範囲外の値を扱う場合のルールは決まるんですね。
ちょっと変な気もするのだが、ここにはいろいろ事情もあるよう。
int32_t型の変数x,y について (x-y)>10 という条件判定を行うことを考える。
ここで x=-2147483630, y=2147483640 の場合、
単純な数学的な計算では x-y=-4294967270 となり 10より小さい。
しかし、-4294967270 はint32_t型の範囲を超えてしまう。
2の補数形式で表して32bitに入らない部分は切り捨てると 26 となるので、
もしそのようにして計算した結果で大小比較を行えば10より大きいということになる。
果たしてどちらをお望みかというのは一概には言えない話だが、
符号付き整数型のように最大値に1加えると最小値に戻る、
というような考え方を適用すると後者の挙動となる。
32bitのプロセッサだと、結局は32bitにして大小比較しないといけない。
なので、ほぼ後者の挙動しか考えられないところである。
ただ、64bitのプロセッサの場合、64bitで演算して大小比較するという方法もとれる。
この場合は前者のような挙動になることも考えられる。コンパイラ次第である。
(int32_t)( ((uint32_t)x) – ((uint32_t)y) )>10 と書けば、確実に後者の挙動となる。
x=-2147483630は uint32_t型では 2147483666 となり、
2147483666-2147483640=26 なので 10より大となる。
なので、まさにこういう書換をしたんですよね。
そしたら、実行コードとしては何も変わらないものが生成された。
最適化レベルにも依存するのかもしれないが、32bitのプロセッサではほぼこういう動きしかしないのだろう。
というわけで思った挙動をするコードは元々生成できていたことが確認出来たと。
今後、コンパイラの設定変更や移植があっても同等の処理になることは保証できると。
(処理系定義の動作があるので、そこに差分がないのが前提だが)
手元にあった64bitプロセッサのコンパイラでも同じことをやってみたが、
こちらは一例だけではあるが、そちらも同じコードが生成できていた。
これこそ最適化レベルや前後のコード次第の面もあるとは思うが。
なのでこれが問題になるケースって本当にあるのかな? と思ってしまうが、
forループのループ変数で発生した場合とか、実際に問題になるケースはいろいろあるらしい。
今回のケースでは特に問題はないということが確認出来たということである。