이런 연산을 clamp 또는 Unsigned Saturate라고 함
멀티미디어 특히 비디오 데이터를 처리하려면 굉장히 빈번하게 사용되는 함수가 바로 clamping이다.
각종 변환 결과 애매하게 이 범위를 벗어나는 경우가 발생할 수 있기 때문에 모든 픽셀에 대해 이 연산을 돌려야 하기 때문이다.
Saturation의 그래프 (뽀샵이 귀찮아 255만…)
1. SSE2를 사용하지 않는 경우-1
기본적으로는 아래와 같이 작성하면 된다. (1A: if #1)
inline unsigned char Clamp(float f) {
int n = (int)f;
if (n < 0)
return 0;
else if (n > 255)
return 255;
return (unsigned char)n;
}
그런데, if문이 두 개 씩이나 들어있어 뭔가 보기 좋지 않다.
if문 대신 삼항연산자를 둘 사용하면 아래와 같이 좀 더 간결하게 쓸 수 있다. (1B: 삼항)
inline unsigned char Clamp(float f) {
int n = (int)f;
n = (n > 255) ? 255 : n;
return (n < 0) ? 0 : (unsigned char)n;
}
그런데, 생각해보면 대부분의 경우 인수 n은 0~255 범위 내에 있다.
즉, 아래와 같이 쓰면 좀 더 성능 향상을 기대할 수 있다. (1C: if #2)
inline unsigned char Clamp(float f) {
int n = (int)f;
if ((unsigned) n <= 255) {
return (unsigned char)n;
}
return (n < 0) ? 0 : 255;
}
비교 연산자보다는 비트 연산자가 조금 더 빠르다는 점을 이용해서 이렇게 쓸 수도 있다. (1D: if #3)
inline unsigned char Clamp(float f) {
int n = (int)f;
if (n & -256) {
return (n < 0) ? 0 : 255;
}
return (unsigned char)n;
}
2. SSE2를 사용하지 않는 경우-2
그런데, if문을 사용하지 않는 방법은 없을까?
물론 있다. 아래와 같은 코드를 사용하면 된다. (2A: fabs)
inline unsigned char Clamp(float f) {
float temp = f + 255.0f - fabs(f - 255.0f);
return (unsigned char)(((int)(f + fabs(temp))) / 4);
}
이 코드는 아래의 식을 활용한 것이다.
min(a,b) = (a + b - abs(a-b)) / 2
max(a,b) = (a + b + abs(a-b)) / 2
fabs()를 CPU에서 빠르게 처리할 수 있는 x86/x64 환경 등에서는 상당한 성능을 보여줄 수 있다.
그리고, 최소값이 0이라는 점을 이용하면 조금 더 간결하게 쓸 수 있다.
이 코드를 int 단위로 처리하면 아래와 같다. (2B: abs)
inline unsigned char Clamp(float f) {
int n = (int)f;
int temp = n + 255 - abs(n - 255);
return (unsigned char)((n + abs(temp)) / 4);
}
위와 같이 함수를 사용하지 않고 기본 연산자만 사용하는 방법도 있다.
아래와 같은 코드를 사용하면 된다. (2C: and/or #1)
inline unsigned char Clamp(float f) {
int n = (int)f;
n &= -(n >= 0);
return (unsigned char)(n | ~-!(n & -256));
}
이 코드는 두 단계로 구분해서 읽을 수 있다.
첫번째 줄에선 0보다 작으면 0으로 만들고, 두번째 줄에선 255보다 크면 255로 만들어준다.
그런데, 두번째 줄은 좀 복잡하다.
이 코드는 아래와 같이 간결하게 쓸 수 있다. (2D: and/or #2)
inline unsigned char Clamp(float f) {
int n = (int)f;
return (unsigned char)((-(n >= 0) & n) | -(n >= 255));
}
위에 기술한 방식들 중 가장 빠른 속도를 보이는 건 재미있게도 if()문을 사용한 방식인 1D였다.
if() 문의 사용을 두려워할 필요 없음, 최적화하는 게 중요할 뿐
3. SSE2를 사용하는 경우
SSE2용 코드는 일단 아래와 같이 쓸 수 있다.
inline void Clamp(__m128 f, unsigned char *dst) {
static const __m128 numF000 = _mm_setzero_ps();
static const __m128 numF255 = _mm_set1_ps(255.0f);
f = _mm_max_ps(f, numF000);
f = _mm_min_ps(f, numF255);
__m128i i = _mm_cvttps_epi32(f);
unsigned char *temp = (unsigned char*)(&i);
dst[0] = temp[0];
dst[1] = temp[4];
dst[2] = temp[8];
dst[3] = temp[12];
}
그런데, 앞 2번 항목의 마지막 코드 2D는 그대로 SSE2에 적용할 수 있다.
주목할 점 하나는 -(n >= 0) 부분.
C/C++의 표준들에서는 bool값 true를 1로 규정하고 있다.
즉, 이 식은 조건식이 참이면 결과가 0xffffffff가 된다.
그런데, SSE2 연산에서는 비교 결과가 참이면 1이 아니라 0xffffffff이다.
따라서 연산 한 번이 줄어든다.
이를 적용하면 아래와 같이 된다.
inline void Clamp(__m128 f, unsigned char *dst) {
static const __m128i nIm1 = _mm_set1_epi32(-1);
static const __m128i nI254 = _mm_set1_epi32(254);
__m128i n = _mm_cvttps_epi32(f);
//(-(n >= 0) & n) | -(n >= 255);
__m128i t1 = _mm_cmpgt_epi32(n, nIm1);
t1 = _mm_and_si128(n, t1);
__m128i t2 = _mm_cmpgt_epi32(n, nI254);
t2 = _mm_or_si128(t1, t2);
unsigned char *temp = (unsigned char*)(&t2);
dst[0] = temp[0];
dst[1] = temp[4];
dst[2] = temp[8];
dst[3] = temp[12];
}
하지만, 사실 SSE2에선 이런 거 필요 없다.
_mm_packus_epi16()가 Unsigned Saturate를 수행한다.
이를 적용하면 아래와 같이 간결한 코드가 나온다.
inline void Clamp(__m128 f, unsigned char *dst) {
__m128i n = _mm_cvttps_epi32(f);
n = _mm_packus_epi16(n, n);
unsigned char *temp = (unsigned char*)(&n);
dst[0] = temp[0];
dst[1] = temp[2];
dst[2] = temp[4];
dst[3] = temp[6];
}
이 결과를 _mm_stream_si128()를 이용해서 stream으로 메모리에 복사하는 게 경험상 가장 빨리 동작했다.
심심해서 풀어본 "미래네 집 현관 비밀번호" (0) | 2016.12.04 |
---|---|
MS Excel의 YEARFRAC() 동작 방식 (0) | 2016.09.04 |
jpeg/png 이미지의 해상도를 간단히 읽어내려면… (0) | 2015.02.16 |
Sqrt(2)의 수렴 (2) | 2015.01.18 |
XOR로 계산하는 암호 해독 (프로젝트 오일러 #59) (0) | 2015.01.05 |