더블을 32 비트 정수로 반올림하는 빠른 방법 설명


169

Lua의 소스 코드를 읽을 때 Lua가 a macro를 사용 double하여 32 비트로 반올림하는 것을 알았 습니다 int. 을 추출하면 macro다음과 같습니다.

union i_cast {double d; int i[2]};
#define double2int(i, d, t)  \
    {volatile union i_cast u; u.d = (d) + 6755399441055744.0; \
    (i) = (t)u.i[ENDIANLOC];}

여기서 엔디안 , 리틀 엔디안, 빅 엔디안ENDIANLOC 으로 정의됩니다 . 루아는 엔디안을 신중하게 처리합니다. 또는 같은 정수 유형을 나타냅니다 .01tintunsigned int

나는 약간의 연구를했고 macro같은 생각을 사용 하는 더 간단한 형식이 있습니다 .

#define double2int(i, d) \
    {double t = ((d) + 6755399441055744.0); i = *((int *)(&t));}

또는 C ++ 스타일로 :

inline int double2int(double d)
{
    d += 6755399441055744.0;
    return reinterpret_cast<int&>(d);
}

이 트릭은 IEEE 754를 사용하는 모든 컴퓨터에서 작동 할 수 있습니다 (현재 거의 모든 컴퓨터를 의미 함). 양수와 음수 모두에서 작동하며 반올림은 Banker 's Rule을 따릅니다 . (이것은 IEEE 754를 따르기 때문에 놀랍지 않습니다.)

나는 그것을 테스트하기 위해 작은 프로그램을 작성했다.

int main()
{
    double d = -12345678.9;
    int i;
    double2int(i, d)
    printf("%d\n", i);
    return 0;
}

그리고 예상대로 -12345679를 출력합니다.

이 까다로운 macro작동 방식에 대해 자세히 알고 싶습니다 . 마법의 숫자 6755399441055744.0는 실제로 2^51 + 2^52또는 1.5 * 2^52이며 1.5이진수로 표시 할 수 있습니다 1.1. 32 비트 정수 가이 마법 번호에 추가되면 여기서 잃어 버렸습니다. 이 트릭은 어떻게 작동합니까?

추신 : 이것은 Lua 소스 코드 Llimits.h에 있습니다.

업데이트 :

  1. @Mysticial이 지적 했듯이이 방법은 32 비트로 제한되지 않으며 숫자가 2 ^ 52 범위에있는 int한 64 비트로 확장 될 수도 있습니다 int. ( macro몇 가지 수정 이 필요합니다.)
  2. 일부 자료에 따르면 Direct3D 에서는이 방법을 사용할 수 없습니다 .
  3. x86 용 Microsoft 어셈블러로 작업 할 때 더 빨리 macro기록됩니다 assembly(이것은 Lua 소스에서도 추출 됨).

    #define double2int(i,n)  __asm {__asm fld n   __asm fistp i}
  4. 단정도 숫자와 비슷한 마법 번호가 있습니다. 1.5 * 2 ^23


3
무엇에 비해 "빠른"?
코리 넬슨

3
@CoryNelson 간단한 캐스트에 비해 빠릅니다. 이 방법은 SSE 내장 함수로 올바르게 구현 될 경우 캐스트보다 문자 그대로 100 배 빠릅니다. (이것은 다소 비싼 변환 코드에 대한 불쾌한 함수 호출을 호출합니다)
Mysticial

2
맞다 – 나는 그것이 더 빠른 것을 볼 수있다 ftoi. 그러나 SSE를 사용하고 있다면 왜 단일 명령을 사용하지 CVTTSD2SI않습니까?
코리 넬슨

3
@tmyklebu 많은 사용 사례 double -> int64가 실제로 2^52범위 내에 있습니다. 부동 소수점 FFT를 사용하여 정수 컨볼 루션을 수행 할 때 특히 일반적입니다.
신비주의

7
@MSalters 반드시 그런 것은 아닙니다. 캐스트는 오버플로 및 NAN 사례의 적절한 처리를 포함하여 언어 사양에 따라야합니다. (또는 컴파일러가 IB 또는 UB의 경우에 지정한 것) 이러한 검사는 비용이 많이 드는 경향이 있습니다. 이 질문에 언급 된 트릭은 그러한 경우를 완전히 무시합니다. 따라서 속도를 원하고 응용 프로그램이 그러한 코너 케이스를 신경 쓰지 않거나 (또는 ​​결코 만나지 않는 경우)이 해킹은 완벽하게 적합합니다.
신비주의

답변:


161

A double는 다음과 같이 표현됩니다 :

이중 표현

두 개의 32 비트 정수로 볼 수 있습니다. 이제 int코드의 모든 버전에서 찍은 (32 비트라고 가정 int)은 그림의 오른쪽에있는 것이므로 결국 32 비트의 가수를 취하는 것입니다.


이제 매직 넘버로; 올바르게 언급했듯이 6755399441055744는 2 ^ 51 + 2 ^ 52입니다. 이러한 숫자를 추가하면 double2 ^ 52에서 2 ^ 53 사이의 "달콤한 범위"로 들어가게됩니다. 여기 Wikipedia 에서 설명했듯이 흥미로운 속성이 있습니다.

2 사이 52 = 4,503,599,627,370,496과 2 53 = 9,007,199,254,740,992 사이에서 표현 가능한 숫자는 정확히 정수입니다

가수는 52 비트 폭이라는 ​​사실에서 비롯됩니다.

2 51 추가에 대한 다른 흥미로운 사실 +2 52를 우리가 가장 낮은 32 비트만을 취하기 때문에 어쨌든 버려지는 두 개의 최상위 비트에서만 가수에 영향을 미친다는 것입니다.


마지막으로 : 부호.

IEEE 754 부동 소수점은 크기와 부호 표현을 사용하는 반면 "정상"기계의 정수는 2의 보수 산술을 사용합니다. 여기서 어떻게 처리합니까?

우리는 양의 정수에 대해서만 이야기했습니다. 이제 32 비트로 표현할 수있는 범위에서 음수를 처리한다고 가정하면 int(-2 ^ 31 + 1)보다 절대 값이 적습니다. 호출 -a. 이러한 숫자는 매직 넘버를 추가하여 분명히 양수이며 결과 값은 2 52 +2 51 + ( -a )입니다.

이제 가수를 2의 보수 표현으로 해석하면 무엇을 얻을 수 있습니까? 2의 보수 합 (2 52 +2 51 )과 (-a) 의 결과 여야합니다 . 다시 말하지만, 첫 번째 항은 상위 2 비트에만 영향을 미치며, 0 ~ 50 비트에 남아있는 것은 2의 보수 표현 (-a)입니다 (다시, 상위 2 비트 빼기).

왼쪽의 여분의 비트를 잘라 냄으로써 2의 보수 수를 더 작은 폭으로 줄이는 것이 이루어 지므로, 더 낮은 32 비트를 취하면 32 비트, 2의 보수 산술에서 정확하게 (-a)가됩니다.


"" "2 ^ 51 + 2 ^ 52를 추가 할 때의 또 다른 흥미로운 사실은 두 개의 가장 높은 비트에서만 가수에 영향을 미친다는 것입니다. 우리는 가장 낮은 32 비트 만 취하기 때문에 버려집니다." ""무엇입니까? 이것을 추가하면 모든 가수를 이동할 수 있습니다!
YvesgereY

@ 존 : 물론, 그것들을 추가하는 요점은 그 값을 그 범위에 두도록하는 것입니다. 이는 분명히 원래 값에 대해 가수를 (다른 것들 사이에서) 이동시킬 수 있습니다. 여기서 말한 것은 일단 해당 범위에 들어가면 해당 53 비트 정수와 다른 비트는 비트 51과 52이며 어쨌든 버려집니다.
Matteo Italia

2
변환하고자하는 사람들 int64_t은 가수를 왼쪽에서 오른쪽으로 13 비트 씩 이동하여 그렇게 할 수 있습니다. 이렇게하면 '매직'숫자에서 지수와 두 비트가 지워지지 만 부호를 유지하고 부호있는 전체 64 비트 정수로 전파합니다. union { double d; int64_t l; } magic; magic.d = input + 6755399441055744.0; magic.l <<= 13; magic.l >>= 13;
Wojciech Migda 23시 46 분
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.