반응형

이런 연산을 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으로 메모리에 복사하는 게 경험상 가장 빨리 동작했다.



반응형

공유하기

facebook twitter kakaoTalk kakaostory naver band