2.3-Integer_Arithmetic
2개의 양수를 더했는데 결과값이 음수이거나,
x < y
와 x - y < 0
의 결과가 다른 등 종종 컴퓨터는 연산 결과가 상식적이지 않다.
이는 컴퓨터가 표현할 수 있는 수의 범위가 한정적인데서 오는 특징이다.
이러한 특징을 아는 것은 신뢰할 수 있는 코드를 짜는 기반이 된다.
하지만,
Lisp 같은 일부 언어는 임의 size 의 산수 연산을 지원하지만,
보통의 언어는 고정된 size 에 대해 지원한다.
새로운 더하기 연산자로
이 연산자는
이는 사실 더하기 연산 이후,
unsigned 와 int 간의 암시적 형변환을 이유로 허락되지 않은 kernel 데이터에 접근할 수도 있었다.
산수연산이 해당하는 범위 내로 결과가 나오지 않을 때,
따라서,
Group 에서 교환조건(Commutative) 가 추가된 Group 이다.
Group은 어떤 집합이 특정 연산에 대해,
가 성립하는 경우, 해당 연산에 대해 Group 이라고 칭한다.
Group이기 때문에 역원이 존재함이 보장되고 이를
이 때,
로 계산할 수 있다.
2의 보수에서는
즉,
2의 보수에서는 양극단, 즉 너무 작거나, 클 때의 연산을 잘 정의해야한다.
해당 연산에서
너무 큰수에 대해서는 modulo 값이 동일한 채로 줄여야하므로
2의 보수 표현과 부호없는 정수의 표현 방법이 동일하기 때문에,
피연산자를 부호없는 정수 표현으로 바꿔서 더하기 연산을 실행하고, 다시 2의 보수 표현으로 바꿔도 동일한 값을 얻을 수 있다. 수식으로 정리하면 다음과 같다.
2의 보수법에서는 OverFlow 는
int tadd_ok(int x, int y) {
int sum = x + y;
return (sum - x == y) && (sum - y == x);
}
위의 예제는 항상 1
을 반환한다.
2의 보수 덧셈은 아벨군이기 때문에, 더하기 연산에 대해 닫혀있다.
따라서, 덧셈의 결과로는 OverFlow 를 감지할 수 없다. 위의 x, y, sum 의 상태를 판단해야한다.
int tsub_ok(int x, int y) {
return tadd_ok(x, -y);
}
이 함수는
(
따라서
원래는
즉, 이론상
C언어에서는 하위
이는 사실 곱한 값에
원래는
즉, 이론상
C언어에서는 하위
이는 사실 곱한 값에
사실, Unsigned 에서의 곱하기와 2의 보수에서의 곱하기는 동일하게 작동한다.
양쪽 모두,
간단하게, 곱셈 연산의 오른쪽 숫자의 비트패턴을 살펴보며, 0이면 패스, 1이면 왼쪽 숫자를 그래도 더하고, 오른쪽 숫자가 왼쪽으로 1 shift 한다.
그리고, Unsigned 와 2의 보수법 모두 같은 패턴을 다르게 해석하는 해석의 차이이기 때문에,
같은 비트를 같은 연산으로 처리하므로, 해석법만 맞추면 동일한 값이 나온다.
즉, Unsigned * Unsigned
의 결과값을 bit 로 표현한 것은 signed * signed
을 bit 로 표현한 것과 같다.
곱하기에 대한 컴파일러 최적화를 위해 상수에 대한 곱을 처리한 다음, 남은 부분을 더하는 것이다.
이를 위해 먼저
부호 없는 정수 표현에서
성립하는 이유는
이 때,
shift 연산도 동일하므로 두 과정을 동일하게 볼 수 있다.
즉,
이다.
2의 보수법에서도 bit-level 연산은 동일하므로,
이다.
두 경우 모두, OverFlow 가 발생할 수 있다는 점을 조심해야한다.
컴파일러는 최적화를 위해, 곱셈을 shifting and adding 으로 처리한다.
예를 들어,
또는 반대로,
(x << (n + 1)) - (x << m)
해당 연산 방법에 대해, n 이 MSB 이면 어떻게 되는가?
따라서,
나누기는 곱셉보다도 훨씬 느리다. 이번에는 오른쪽 shift 를 통해 해결할 수 있다.
logical, arithmetic 은 부호 없는 표현, 부호 있는 표현 각각에 대해 처리해준다.
정수 나눗셈은 항상
이를 명확히 하기 위해 표기를 정리한다.
양수 결과에 대해서는 round down(
음수 결과에 대해서는 round up(
<<
은 비트를 왼쪽으로 밀면서, 새로운 공간을 0으로 채운다.
ex)
그러나, >>
의 경우는 조금 다르게 작동한다. 크게,
signed int
에서 유용하다.
C언어 표준은 Logical, Arithmetic 을 특정하지 않는다.
단, 대부분의 컴파일러가 signed data
에 대해 Arithmetic 을 사용하고, 개발자도 그렇게 받아들인다.
하지만, unsigned data
에 대해서는 Logical 을 사용해야만 한다.
Java 에서는 >>
을 Arithmetic 으로, >>>
을 Logical 로 이용한다.
C언어에서는 다음이 성립한다.
이 때,
따라서,
또한,
2의 보수법 즉, 부호가 있는 정수에 대한 나눗셈은 조금 더 복잡하다.
우선, 음수에 대해 부호 때문에 MSB 가 보존되는
2의 보수법으로 표현된 값
양수에 대해서는 MSB 가 0이므로,
2의 보수에서
이 때,
따라서,
하지만,
shift 의 결과는
2의 보수법의 음수에 대해서는
이는 C언어에서 다음으로 표현된다.
이를 더하는 것은 floor
와 의미가 일치한다.
또는 다음의 수식으로 이해할 수 있다.
이는 다음으로 증명가능하다.
이러한 분석을 통해, 2의 보수 표현에 대해
(x < 0 ? x + (1 << k) - 1: x) >> k = x/2^k
이다.
function returns x/16 for integer x.
"No" Division, modulus, multiplication, any conditional, any comparison, any loop.
x is type int with 32bits, use two's complements. And right shift are performed "arithmetics."
int div16(int x) {
int bias = (x >> 31) & 0xF;
return (x + bias) >> 4;
}
중요한 포인트는 양수에 대해서는 bias = 0
이며, 음수에 대해서는 16bit의 111...1
이다.
이는 양수, 음수가 MSB 에 의해 판단되며, arithmetic right shift 가 MSB 를 복사한다는 점을 이용한다.
2의 보수에서
대부분의 machine 에서 양쪽 연산을 모두 지원하는 이유 중 하나일 것이다.
하지만, 나눗셈에 대해서는 일반적인 방법이 없다.
# define M
#define N
int arith(int x, int y) {
int result = 0;
result = x*M + y/N;
return result;
}
// compile and deCompile...
int optarith(int x, int y) {
int t = x;
x <<= 5;
x -= t;
if(y < 0) y += 7;
y << 3; /* arithmetic shift */
return x + y;
}
Answer:
해당 과정을 정리하면,
"정수" 연산은 모듈러 연산의 일종이다.
한정된
2의 보수법은 부호 없는 정수와 똑같은 bit-level 연산을 지원해주는 방법이다.
bit-level 연산에서는 같거나 거의 비슷했다.
C언어의 미묘한 행동은 예상치 못한 결과를 낳는다.