iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
📚

Signed Integers in C Are Not Modular Arithmetic

に公開

It is sometimes explained that addition, subtraction, and multiplication of signed integers in C are "modular arithmetic modulo 2^n," but this is incorrect. The correct explanation is that "overflow of signed integers in C is undefined behavior."

Here are two examples where this difference can be observed.

First, let's look at unary minus. Consider the following program:

#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
bool f(int x)
{
    return x == -x;
}
int main()
{
    printf("%s\n", f(INT_MIN) ? "true" : "false");
}

If unary minus for the int type were performed using modular arithmetic modulo 2^n, there should be two values of x for which f returns true: 0 and INT_MIN. However, since overflow of unary minus for int is actually undefined behavior, the compiler can perform optimizations without considering the case where x = INT_MIN.

In fact, with Clang, the execution result of the above program changes depending on whether optimization is enabled:

$ clang -o test1 test1.c
$ ./test1
true
$ clang -o test1 -O2 test1.c
$ ./test1
false

(The fact that the behavior of x == -x changes due to compiler optimization was something the author of LuaJIT pointed out to me when I opened an issue: Undefined behavior with negation of INT*_MIN · Issue #928 · LuaJIT/LuaJIT)

As another example, consider the following program involving multiplication:

#include <stdbool.h>
#include <stdio.h>
bool g(int x)
{
    return x * x >= 0;
}
int main()
{
    printf("%s\n", g(65535) ? "true" : "false");
}

Assume that the width of int is 32 bits. If multiplication for the int type were performed using modular arithmetic modulo 2^{32}, then when x = 65535, x * x would be 65536^2=4294836225. Since this is at least 2^{31}=2147483648 and less than 2^{32}=4294967296, it should wrap around to -131071. However, since overflow of int multiplication is actually undefined behavior, the compiler can perform optimizations without considering overflow.

In fact, with Clang, the execution result of the above program changes depending on whether optimization is enabled:

$ clang -o test2 test2.c
$ ./test2
false
$ clang -o test2 -O2 test2.c
$ ./test2
true

Note that for unsigned integers in C, addition, subtraction, and multiplication are guaranteed to be performed as modular arithmetic modulo 2^n. Also, operations with overflow checking added in C23 can yield wrapped-around results even for signed integers:

Finally, the version of the compiler I used for verification is as follows:

$ clang --version
Apple clang version 14.0.0 (clang-1400.0.29.202)
Target: arm64-apple-darwin21.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

Discussion