TEUS.me

 
 

이런 연산을 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

본문과 관련 있는 내용으로 댓글을 남겨주시면 감사하겠습니다.

비밀글모드

  1. bool값이 정수로 바로 나오는군요.
    표준에서 배제될 테니... int() 라고 하라고 해서, 그렇게 해 왔는데...
    bool b = true;
    int i;
    i = int( b );
    복잡해도... 이렇게 쓰라고 하더군요.
    2016.04.02 18:01
    • 표준이 바뀌면 그게 맞에 몽땅 뜯어고쳐야 됩니다.
      애초에 true를 1로 규정했으니 가능한 꼼수죠.

      만약, 표준이 바뀌면서 아예 "0이 아님"으로 바뀌면 헬게이트 오픈…
      2016.04.02 19:53 신고
  2. 예. 몽땅 뜯어고쳐야 하지요.

    아무튼 true 값이 "0 아님"이라고 바뀐다고 해서...
    i = int(b); 를 고집했는데... 쓸데없는 짓이었다니... ㅜㅜ;;
    뭐, 앞으로 편해지겠네요. 아주 조금이지만...
    2016.04.03 18:20