- 큰 정수 덧셈은 보통 64비트 limb로 쪼개 처리하지만, 자리올림 전파가 생기면 현대 CPU의 병렬 실행 이점을 제대로 쓰기 어려움
- x86의
adc는 이전 연산의 carry flag에 의존해 명령 체인을 직렬화하므로, Intel Haswell처럼 여러add를 병렬 실행할 수 있는 구조에서도 병목이 됨 - radix 2^51 표현은 256비트 값을 네 개의 2^64 자리 대신 다섯 개의 2^51 자리로 나눠, 각 limb의 남는 상위 비트를 중간 자리올림 저장 공간으로 활용함
- 자리올림을 제거하는 방식은 아니며, 여러 번의 덧셈 동안 전파를 지연한 뒤 마지막 정규화 단계에서 한꺼번에 처리함
- Haswell에서의 간단한 벤치마크에서는 변환 비용을 포함해도 세 번의 덧셈부터 radix 2^64 방식보다 빨랐고, 반복 횟수가 늘수록 이점도 커짐
큰 정수 덧셈에서 자리올림이 병목이 되는 이유
- 종이에 하는 긴 덧셈은 1의 자리부터 오른쪽에서 왼쪽으로 진행함
- 각 자리의 결과가 오른쪽 자리에서 넘어오는 자리올림에 의존하기 때문임
- 왼쪽부터 더하면 나중에 발생한 자리올림 때문에 이미 계산한 앞자리 결과를 다시 고쳐야 함
- 큰 정수 덧셈도 같은 제약을 가짐
- 256비트 정수
x와y를 네 개의 64비트 limb로 나누면 같은 위치의 limb끼리 더할 수 있음 - 낮은 limb에서 오버플로가 나면 그 1을 더 높은 limb로 넘겨야 함
- 256비트 정수
- x86의
adc는 이 전파를 처리하는 명령임- 이전 연산의 오버플로 여부를 보고 필요한 경우 1을 더함
- 올바른 256비트 덧셈은 최하위 limb부터
add,adc,adc,adc순서로 이어짐
adc가 현대 CPU에서 느려지는 구조
adc는 대체로 일반add보다 실행 비용이 큼adc는 carry flag라는 세 번째 입력을 사용하므로add보다 복잡함add보다 덜 자주 쓰이기 때문에 CPU 설계자가adc성능 최적화에 칩 면적을 투입할 유인이 작음
- 더 큰 문제는 명령 의존성임
- Intel Haswell에서 단일
add는 실행에 1사이클이 걸림 - 이상적인 조건에서는 Haswell이 한 사이클에 최대 4개의
add를 실행할 수 있음 - Haswell에는 8개의 실행 포트가 있고, 그중 4개가 정수
add를 실행할 수 있음
- Intel Haswell에서 단일
- 독립적인
add네 개는 병렬 실행되기 쉬움- 반면
adc체인은 각 명령이 이전 명령의 carry flag 출력에 의존함 - CPU는 이 명령들을 병렬화하지 못하고 순서대로 실행해야 함
- 반면
- SIMD에서는 손실이 더 커짐
vpaddq는 네 개의 64비트 덧셈을 동시에 수행함- Haswell은 한 사이클에 두 개의
vpaddq를 실행할 수 있음 - 자리올림 처리를 위해 이 병렬성을 포기하면 성능 이점이 줄어듦
종이 덧셈으로 보는 자리올림 지연
- 10진 자리값은 유지하되, 각 자리에 들어갈 수 있는 문자를 넓히면 자리올림을 늦출 수 있음
- 일반
0-9대신A-Z,*까지 더해 총 37개 문자를 사용함 - 하지만 진법 자체는 37진법이 아니라 여전히 10진 자리값을 유지함
- 일반
- 한 자리가 9를 넘어도 즉시 자리올림할 필요가 없어짐
29 + 1은30으로 쓸 수도 있지만2A,1K,U처럼도 표현 가능함- 두 수의 각 자리가 모두 9 이하로 정규화되어 있다면 덧셈 중 자리올림을 미룰 수 있음
- 모든 입력에 항상 적용되지는 않음
9 + W처럼 이미 큰 자릿값이 들어 있으면 자리올림이 필요함- 정규화된 수끼리는 최대 네 개까지 더해도 자리올림 없이 표현 가능함
- 마지막에는 다시 일반 10진 표현으로 정규화해야 함
- 오른쪽부터 각 자리에서 몇 개의 10이 들어 있는지 계산함
- 그만큼을 현재 자리에서 빼고 다음 자리로 넘김
- 핵심은 자리올림 전파를 없애는 것이 아니라, 중간 계산 동안 저장해 두고 마지막에 한 번 전파하는 데 있음
컴퓨터에서의 radix 2^51 표현
- 256비트 값을 네 개의 2^64 limb로 나누면 각 limb는 0부터 2^64−1까지 값을 가질 수 있음
- 각 limb를 2^64 진법의 자리로 보는 방식임
- 하드웨어의 64비트 정수 범위는 넓힐 수 없으므로 진법의 크기를 줄임
- 256비트 값을 네 개의 2^64 자리 대신 다섯 개의 2^51 자리로 나눔
- 각 limb는 여전히 64비트 정수로 저장되지만 실제 값은 51비트 또는 52비트만 사용함
- 남는 상위 비트는 중간 자리올림 저장 공간이 됨
- 각 limb에는 원래 숫자의 51비트 또는 52비트가 들어감
- 나머지 12비트 또는 13비트가 계산 중 발생한 자리올림을 담음
- 이 기법은 암호학 문헌에서 radix 2^51 representation으로 불림
- 정규화된 수라면 2^64개의 가능한 limb 값 안에서 최대 2^13개를 더하기 전까지 상위 13비트 오버플로를 걱정하지 않아도 됨
52비트 최상위 limb와 정규화
- 최상위 limb에는 52비트를 할당함
- 나머지 limb는 51비트를 사용함
- 최상위 limb의 자리올림은 무시해 2^256−1을 넘는 경우 감싸도록 처리함
- 이는 C의 일반 크기 unsigned 정수 덧셈이 오버플로 시 감싸는 방식과 같음
- radix 2^51 덧셈 코드는
adc체인을 쓰지 않고 다섯 개의add를 독립적으로 실행함- 네 개의 2^64 limb 방식보다
add수는 4개에서 5개로 늘어남 - 대신 carry flag 의존성이 없어 병렬 실행이 가능함
- 네 개의 2^64 limb 방식보다
- 정규화 단계에서는 각 limb의 상위 비트를 꺼내 다음 상위 limb에 더함
shr 51로 carry 부분을 추출함and 0x0007FFFFFFFFFFFF로 51비트 아래만 남김- 최상위 limb는
and 0x000FFFFFFFFFFFFF로 정리함
- 정규화는 지연해 둔 자리올림 전파를 마지막에 수행하는 단계임
- 중간 덧셈에서는 carry flag 의존성을 만들지 않음
- 최종적으로 각 limb를 다시 허용 범위 안으로 맞춤
성능 결과와 뺄셈 확장
- 간단한 벤치마크에서 radix 2^51 덧셈은 Haswell CPU에서 더 빠른 결과를 보임
- radix 2^51 표현으로의 변환과 복귀 비용까지 포함함
- 세 번의 덧셈만으로도 radix 2^64 덧셈보다 빨랐음
- 덧셈 횟수가 늘수록 절감 효과도 함께 커짐
- 같은 아이디어는 뺄셈에도 확장할 수 있음
- 뺄셈에서는 자리올림이 음수 carry가 됨
- 뺄셈을 지원하려면 limb를 unsigned가 아니라 signed 정수처럼 다룸
- 각 자리값은 양수 또는 음수가 될 수 있음
- 각 limb는 양수 carry와 음수 carry를 모두 저장할 수 있음
- 이 변경에는 비용이 따름
- 각 limb의 최상위 비트가 부호 비트로 예약됨
- 정규화 사이에 수행 가능한 연산 수가 2^13에서 2^12로 줄어듦
- 데이터를 더 많은 레지스터에 나누고 연산 수가 늘어나더라도, 자리올림 의존성을 줄이면 전체 성능이 개선될 수 있음