띠용? 1374389535 이라는 숫자는 도대체 어디서 온 것일까요? 그리고 왜 이번에는 div 명령어를 사용하지 않을까요?
div 는 느리다
일단 무엇보다도 CPU 에서 나눗셈 연산은 무지하게 느립니다. 기본적으로 덧셈과 뺄셈 연산이 보통 1 사이클 안에 되고 곱셈도 이 보다 살짝 느린 3 ~ 4 사이클 안에 수행됩니다. 그런데 (32 비트) 정수 나눗셈의 경우 20 ~ 26 사이클이 걸리죠. 이 시간 동안 곱셈을 무려 5 번이나 더 할 수 있습니다.
물론 예외는 있습니다. 2 의 지수승으로 나누는 연산은 CPU 가 굉장히 빨리 수행할 수 있습니다. 예를 들어서 25 으로 나눈 다는 것은 사실상 오른쪽으로 쉬프트 5 번 하는 것과 마찬가지 이기 때문입니다. 그리고 CPU 상 쉬프트 연산과 같은 간단한 비트 연산들은 모두 1 사이클 안에 수행됩니다. 엄청나게 빠르죠!
따라서 컴파일러는 되도록이면 나눗셈을 곱셈으로 바꿀 수 있으면 바꾸고, 불가피 하게 나눗셈을 하게 된다면 되도록이면 2 의 지수승으로 나눗셈을 해버리려고 하게 됩니다.
위 경우도 마찬가지 입니다. [100d] 은 사실 [d×1001] 과 똑같죠.
문제는 정수로 1001 을 표현할 길이 없다는 점입니다. 하지만 만약에 분자 분모에 적당히 큰 2n 을 곱해준다면 어떨까요? 예를 들어서 210 을 곱해준다고 생각해봅시다.
[100d]=[100⋅210d⋅210]=[d⋅100210⋅2101]
마지막 ⋅2101 는 2 의 지수승 나눗셈 이므로 쉬프트 연산을 통해 아주 빠르게 수행할 수 있습니다. 그런다면 100210 은 어떨까요? 210 이 1024 이니까, 대충 100210≈11 로 근사해봅시다.
그렇다면 [100d] 을 대충 [d⋅112101] 으로 근사할 수 있을까요? 예를 들어서 d=100 이면 전자의 경우 1 이 되지만 후자의 경우도 마찬가지로 [10241100] 가 되서 1 이 됩니다.
하지만 조금만 생각해보면 작은 d 에 대해서도 오차가 발생한다는 사실을 알 수 있습니다. 에를 들어서 d 가 5000 이라면 전자는 50 이 되지만 후자는 [102455000] 인 53 이 됩니다.
이렇게 오차가 발생하는 이유는 곱해주는 값인 210 이 너무 작기 때문입니다. 그렇다면 얼마나 큰 2 의 지수승을 구해줘야지 d 가 unsigned int 의 범위인 0 부터 4,294,967,295 까지의 모든 정수들에 대해서 올바른 결과값을 돌려줄 수 있을까요?
편의상 2n 을 곱해주었다고 생각해봅시다.
이 경우 우리가 계산하려는 식을 엄밀하게 쓰면
[[1002n+M⋅d]⋅2n1]
이 되는데, M 을 2n+M 이 100 으로 나누어 떨어지게 하는 최소한의 값으로 잡으면 (M≡−2n(mod100)) 첫 번째 [] 안에 항상 정수가 되서 그냥
[1002n+M⋅d⋅2n1]
와 같이 간단히 쓸 수 있습니다. 위 식을 다시 써보면
[1001(d+2nM⋅d)]
라 쓸 수 있습니다.
자, 그러면 원래 계산하려던
[100d]
과 비교해봅시다. 전자와 후자가 int 범위 안의 d 에 대해서 모두 같으려면 n 이 얼마가 되야 할까요? 만약에, 2nM⋅d 이 1 보다 항상 작게 되면 위 두 값은 항상 일치하게 됩니다.
왜냐하면 [99/100] 이나 [99.9999/100] 이나 0 이 되는 것은 매한가지 일테니까요. 반면에 그 차이가 1 이상이 된다면 더이상 같다고 보장할 수 없게 됩니다. 왜냐하면 [99/100] 은 0 이지만 [100/100] 은 1 이니까요!
그런데 d 의 최대값은 232−1 이므로, 우리가 쓰고자 하는 조건은
2nM⋅(232−1)<1
이 되겠습니다. 자, 그럼 n 이 32 이면 충분할까요? 232 는 4294967296 이므로 M 은 4 가 될 것입니다. 하지만 2324⋅(232−1)≈4 가 되어서 조건을 만족시키지 않죠.
조건을 만족하는 n 을 하나씩 차근차근 찾아보면
n=33 : 233 이 8589934592 니까 M=8 이 되서 2338⋅(232−1)≈4>1
n=34 : 234 이 17179869184 니까 M=16 이 되서 23416⋅(232−1)≈4>1
n=35 : 235 이 34359738368 니까 M=32 이 되서 23532⋅(232−1)≈4>1
n=36 : 236 이 68719476736 니까 M=64 이 되서 23664⋅(232−1)≈4>1
n=37 : 237 이 137438953472 니까 M=28 이 되서 23728⋅(232−1)≈0.88<1
따라서 위를 만족하는 최소의 n 은 37 임을 알 수 있습니다. 그리고 곱할 수는 실제로 100137438953472+28 인 1374389535 임을 확인할 수 있죠!
위 방법이 언제나 가능한 것은 아니다.
그렇다면 아래 코드는 어떻게 바꿀 수 있을까요?
intfunc(unsignedint d) { return d /7; }
일단 조건은 M≡−2n(mod7) 이어야 하고, 2nM⋅(232−1)<1 여야 하죠. 이를 만족하는 최소의 n 은 계산해보면 n=35 임을 알 수 있습니다. 따라서 곱해줘야 하는 값은 [7235]+1 인 4908534053 가 됩니다.
한 가지 문제가 있습니다. 4908534053 는 unsigned int 의 최대 범위를 벗어나는 크기 입니다. 즉 232 보다 큰 수이죠. 그런데 이 수를 예를 들어서 231 과 곱하게 되면 그 결과는 264 를 벗어나게 됩니다.
처음에 저는 이 식을 보았을 때 도대체 이게 어떠한 방식으로 유도되었는지 도무지 감을 잡을 수 없었습니다. 다른 수들을 가지고 컴파일 해 보니 공통적으로 위와 같은 꼴의 식으로 컴파일 된다는 사실을 알 수 있었습니다.
아쉽게도 위 형태의 식이 어떠한 방식으로 유추되었는지는 모르겠습니다. 하지만 며칠간의 삽질 끝에 위와 같은 형태의 식이 n<232 인 모든 양의 정수들의 정수 나눗셈을 정확히 계산하는 데에는 충분하다는 사실을 증명할 수 있었습니다. 아래 그 사실을 간단히 소개하고보자 합니다.
정리
적당한 C<232 와 X<32 에 대해서
[2X[2n−[232Cn]]+[232Cn]]=[dn]
가 232 보다 작은 양의 정수 n 에 대해서 항상 성립합니다.
위를 증명하기 위해 먼저 적당한 C 와 X 를 어떻게 찾을 수 있는지 살펴봅시다.
보조 정리
모든 D<232 에 대해서
0≤233+X232+C−D1≤D12321
를 만족하는 C<232 와 X<32 가 항상 존재한다. (즉 적당한 C 와 X 가 있어서 D1 를 충분히 근사할 수 있다는 의미 입니다.)
증명은 다음과 같습니다. 먼저 D<232 이므로 2X≤D<2X+1 이 성립하도록 X 를 잡을 수 있습니다. 편의상 D=2X+t 라고 합시다. 대입을 하자면 C 는 아래 식을 만족해야 하겠죠.
0≤233+X232+C−2X+t1≤2X+t12321
양변에 2X 를 곱해주면
0≤233232+C−1+2Xt1≤1+2Xt12321
위 식을 정리하면 아래와 같은 식을 얻을 수 있습니다.
1+2Xt233≤232+C≤1+2Xt233+2=1+2Xt233+1+2Xt2
자 근데, 우리는 t<2X 라는 점을 알고 있습니다. 따라서 1+2Xt2∈[1,2] 이겠죠. 따라서 일단 1+2Xt233 은 최소한 232 보다 크므로 C>0 을 만족할 수 있습니다. 문제는 정수 C 가 존재하기 위해서는 위 구간의 크기가 1 이상이어야 하는데, 1+2Xt2≥1 이므로, 위 범위 내에 들어가는 정수 C 는 반드시 존재하게 됩니다.
보조 정리를 조금 정리해보자면;
Dn21+X−n≤232Cn≤Dn21+X(1+2321)−n
와 같은 관계식을 얻을 수 있습니다. 따라서
[Dn21+X]−n≤[232Cn]≤[Dn21+X(1+2321)]−n
로 쓸 수 있죠.
자 이제 그렇다면 원래의 정리로 다시 돌아가보겠습니다. 먼저 [x]≤x 가 항상 성립하므로,
댓글을 불러오는 중입니다..