모든 이야기

saturation: 범위 초과를 막았는데 최종 합이 달라지는 이유

8비트 signed 계산에서 100에 60을 더했더니 −96이 나온다. 작은 양수를 두 개 더했는데 큰 음수가 생겼다. 정수 비트 연산의 규칙으로는 자연스러운 결과지만, 밝기나 출력 세기 같은 값을 나타내고 있었다면 의도한 동작과 거리가 멀다. 이때 흔히 선택하는 해법이 saturation, 즉 표현 범위를 벗어나는 값을 끝값에 붙이는 연산이다.

그렇다고 saturation을 켜는 순간 계산이 원래 수학에 가까워지는 것은 아니다. 포화시키는 위치에 따라 나중에 되돌아올 수 있었던 값도 잃는다. 포화 연산은 overflow 처리 방식인 동시에 알고리즘의 일부다.

범위 밖의 답을 어디에 놓을까

signed 8비트의 표현 범위는 −128~127이다. 정확한 정수 합 160을 하위 8비트만 남기면 0xA0이고, 이를 2의 보수로 읽으면 −96이다. 이것이 wraparound다. 반면 signed saturation은 160을 127로 내보낸다. 아래쪽 경계를 넘어 −140이 되었다면 −128로 붙인다.

AMD의 fixed-point 문서는 overflow 모드로 saturation과 wrap을 구분한다. 또한 0으로 보내는 포화, 대칭 범위로 제한하는 포화 등을 별도로 제공한다. ‘포화’라는 이름 하나만으로 하한이 −128인지 −127인지까지 결정되지는 않는다는 뜻이다. 이 글의 예에서는 양 끝이 −128과 127인 통상적인 signed 포화를 사용한다. AMD UG1399 overflow 모드 표

직접 회로를 만든다면 답을 잃기 전에 범위 초과를 찾아야 한다. 8비트 입력 두 개의 합을 signed 9비트로 구한 뒤 127보다 큰지, −128보다 작은지 비교하고 끝값이나 정상 결과를 선택할 수 있다. 이미 8비트로 잘라 −96만 남긴 뒤 127과 비교하면 양의 overflow를 발견하지 못한다.

덧셈의 부호 관계를 써도 된다. 두 입력의 부호가 같고 8비트 결과의 부호가 바뀌었다면 signed overflow다. 양수끼리 더했다면 127, 음수끼리 더했다면 −128을 고른다. 이 간단한 판정은 같은 폭의 2의 보수 덧셈이라는 전제에서 나온다. 곱셈이나 서로 다른 폭의 변환에 그대로 붙일 규칙은 아니다.

중간에 붙인 끝값은 나중에 돌아오지 않는다

가상의 누산기에서 100, 60, −60을 순서대로 더해 보자. 넓은 정수 연산의 정확한 최종 합은 100이다. 최종 합만 8비트 범위에 제한하면 그대로 100을 얻는다.

매 단계에서 saturation을 하면 경로가 달라진다. 먼저 100+60=160을 127로 붙인다. 다음 항 −60을 더하면 67이다. 앞에서 잃어버린 33이 뒤의 음수 항을 만나도 복구되지 않는다. 최종 값이 범위 안이라는 사실이 중간 포화를 정당화하지 못한다.

순서를 달리하여 60+(−60)을 먼저 계산하면 0이고, 여기에 100을 더하면 100이다. 같은 세 입력도 포화 덧셈을 어디에 묶는지에 따라 67과 100으로 갈린다. 일반 정수 덧셈의 결합법칙을 포화 덧셈에 그대로 적용할 수 없다. 병렬 합산 트리를 바꾸거나 누산 구간을 나눌 때 이 차이가 실제 출력 차이로 나타날 수 있다.

흥미롭게도 이 세 항을 동일한 8비트 wrap 규칙으로 끝까지 더하면 최종 비트열은 100으로 돌아온다. 고정된 폭에서 덧셈을 modulo 256으로 수행했기 때문이다. 다만 중간 −96을 비교하거나 다른 계산에 사용했다면 그 관측은 이미 달라진다. 따라서 wrap이 항상 망가진 계산이고 포화가 항상 복구된 계산이라는 판단도 성립하지 않는다.

끝값에 붙는 동안 무엇을 잃는가

출력 세기의 상한을 넘는 값을 모두 최대로 내보내겠다는 계약이라면 포화가 적합할 수 있다. 위의 160과 200은 모두 127이 되어, 입력 크기의 차이를 출력에서 구분할 수 없게 된다. 반대로 원형 카운터처럼 범위를 넘으면 처음으로 돌아가야 하는 값에는 wrap이 목적에 맞는다. 사용되는 숫자의 의미부터 정해야 한다.

피드백 계산에서는 포화가 지속된 시간도 중요하다. 상태값을 끝값에 계속 붙여 놓으면, 상한을 얼마나 넘었는지에 대한 정보가 상태에 남지 않는다. 이후 입력 부호가 바뀌었을 때 언제 정상 범위로 돌아오는지까지 달라질 수 있다. 이 동작이 원하는 제한인지, 넓은 내부 상태와 좁은 출력 제한을 분리해야 하는지 따져야 한다.

포화는 회로 비용도 가진다. 범위 초과 판정과 끝값을 선택하는 논리가 필요하고, 비교와 선택이 조합 경로에 추가될 수 있다. AMD 문서 역시 saturation이 추가 논리를 요구한다고 설명한다. 실제 면적이나 지연 증가량은 폭, 구현 방식, 목표 소자에 따라 측정해야 한다. 문서의 특정 수치를 모든 데이터패스의 비용으로 가져올 수는 없다. AMD UG1399 saturation 관련 주석

또한 범위 제한과 정밀도 손실을 분리해야 한다. 소수부를 줄이는 반올림이 먼저 수행되어 그 결과가 상한을 넘을 수도 있다. 이때 어떤 순서로 반올림과 포화를 적용하는지까지 출력 계약에 들어간다.

saturation을 선택할 때 가장 유용한 질문은 ‘overflow를 막을까’보다 ‘어느 값을 어느 시점에 끝값으로 바꿀까’다. 중간 계산이 보존해야 할 정보와 외부에 허용할 범위를 구분하면, −96을 127로 바꾸는 작은 회로가 전체 계산을 어떻게 바꾸는지 판단할 수 있다.