豪鬼メモ

一瞬千撃

キャストに関わるGCCの謎挙動

GCCの不可解な挙動に悩まされている。以下のコードは、負数を出力しないことが期待されるが、-O2以上の最適化をすると負数が出力されてしまう。

#include <cstdint>
#include <cstdio>

int main(int argc, char** argv) {
  constexpr uint32_t modulo = 2039;
  for (int32_t i = 0; i < 65536; i++) {
    const int32_t result = static_cast<uint32_t>(i * i) % modulo;
    std::printf("%d\n", result);
  }
  return 0;
}

上記で、i の値が46340を超えると、i * i の値がint32_tの範囲をオーバーフローして負数になる。その直後にuint32_tにキャストされると正数に変換され、その後に2039で割った余りになるので、結果は0から2038の間になるはずだ。それをint32_tにキャストしてもオーバーフローはしないはずだ。-O0と-O1では期待通りに動作する。しかし、-O2以上の最適化をすると、負数が生成される。-O2以上の最適化をする場合も、変数moduloの宣言にvolatileをつけると負数は生成されない。

gccのバージョンはgcc (Ubuntu 10.3.0-1ubuntu1~20.10) 10.3.0だ。バグなのか、仕様上で未定義の挙動を引き起こす記述をしてしまっているのか、わからない。キャストが無視されているのはアセンブリ出力を見るまでもなく明らかなのだが、なぜそうなるのかというか、仕様通りなのかどうかが知りたい。「最適化によってキャストが無視されることがある」という事実を認めるとすると、どのような場合にそれが起こるのか知っておかないと安心して暮らせないだろう。ご存知の人がいたら教えてほしい。


追記:情報を寄せていただいた。このページによると、unsignedのオーバーフローは2の補数の結果が得られるが、signedの場合には何が起こってもおかしくないらしい。オーバーフローした該当の演算の値が未定義になるだけでなく、外側のキャスト演算も含めて挙動が変わるとは、恐ろしい。signedのオーバーフローは発生させた時点で負けってことか。オーバーフローしないことを前提として最適化を行えば、キャストが消えるのは自然とも言える。だとすれば、いろいろ不安になる。自分の過去のコードを洗って、signedのオーバーフローの恐れがないか確認せねば。