Metode cepat untuk membulatkan dobel ke int 32-bit dijelaskan

169

Ketika membaca kode sumber Lua , saya perhatikan bahwa Lua menggunakan a macrountuk membulatkan a doubleke 32-bit int. Saya mengekstraknya macro, dan terlihat seperti ini:

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];}

Di sini ENDIANLOCdidefinisikan sebagai endianness , 0untuk little endian, 1untuk big endian. Lua dengan hati-hati menangani endianness. tsingkatan dari tipe integer, seperti intatau unsigned int.

Saya melakukan sedikit riset dan ada format yang lebih sederhana macroyang menggunakan pemikiran yang sama:

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

Atau dalam gaya C ++ -:

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

Trik ini dapat bekerja pada mesin apa pun menggunakan IEEE 754 (yang berarti hampir semua mesin saat ini). Ini bekerja untuk angka positif dan negatif, dan pembulatannya mengikuti Aturan Banker . (Ini tidak mengejutkan, karena mengikuti IEEE 754.)

Saya menulis sebuah program kecil untuk mengujinya:

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

Dan output -12345679, seperti yang diharapkan.

Saya ingin menjelaskan bagaimana cara macrokerja rumit ini . Angka ajaib 6755399441055744.0sebenarnya 2^51 + 2^52, atau 1.5 * 2^52, dan 1.5dalam biner dapat direpresentasikan sebagai 1.1. Ketika bilangan bulat 32-bit ditambahkan ke angka ajaib ini, well, saya hilang dari sini. Bagaimana trik ini bekerja?

PS: Ini dalam kode sumber Lua, Llimits.h .

PEMBARUAN :

  1. Seperti yang ditunjukkan oleh @Mysticial, metode ini tidak membatasi dirinya sendiri menjadi 32-bit int, tetapi juga dapat diperluas menjadi 64-bit intselama angkanya berada dalam kisaran 2 ^ 52. ( macroPerlu beberapa modifikasi.)
  2. Beberapa bahan mengatakan metode ini tidak dapat digunakan dalam Direct3D .
  3. Ketika bekerja dengan Microsoft assembler untuk x86, ada yang lebih cepat macroditulis assembly(ini juga diekstrak dari sumber Lua):

    #define double2int(i,n)  __asm {__asm fld n   __asm fistp i}
  4. Ada angka ajaib serupa untuk nomor presisi tunggal: 1.5 * 2 ^23

Yu Hao
sumber
3
"cepat" dibandingkan dengan apa?
Cory Nelson
3
@CoryNelson Cepat dibandingkan dengan pemain sederhana. Metode ini, ketika diimplementasikan dengan benar (dengan intrinsik SSE) secara harfiah seratus kali lebih cepat dari para pemain. (yang memanggil pemanggilan fungsi tidak menyenangkan ke kode konversi yang agak mahal)
Mysticial
2
Benar - saya bisa melihatnya lebih cepat dari ftoi. Tetapi jika Anda berbicara SSE, mengapa tidak menggunakan instruksi tunggal CVTTSD2SI?
Cory Nelson
3
@tmyklebu Banyak kasus penggunaan yang masuk double -> int64memang dalam 2^52jangkauan. Ini sangat umum ketika melakukan konvolusi bilangan bulat menggunakan FFT titik-mengambang.
Mysticial
7
@MSalters Belum tentu benar. Para pemain harus memenuhi spesifikasi bahasa - termasuk penanganan yang tepat untuk case overflow dan NAN. (atau apa pun yang ditentukan kompilator dalam kasus IB atau UB) Pemeriksaan ini cenderung sangat mahal. Trik yang disebutkan dalam pertanyaan ini sepenuhnya mengabaikan kasus sudut tersebut. Jadi, jika Anda menginginkan kecepatan dan aplikasi Anda tidak peduli (atau tidak pernah menemukan) kasus sudut seperti itu, maka peretasan ini sangat tepat.
Mysticial

Jawaban:

161

A doubledirepresentasikan seperti ini:

representasi ganda

dan itu dapat dilihat sebagai dua bilangan bulat 32-bit; sekarang, yang intdiambil di semua versi kode Anda (seandainya itu 32-bit int) adalah yang di sebelah kanan dalam gambar, jadi apa yang Anda lakukan pada akhirnya hanya mengambil 32 bit mantissa terendah.


Sekarang, ke angka ajaib; seperti yang Anda sebutkan dengan benar, 6755399441055744 adalah 2 ^ 51 + 2 ^ 52; menambahkan angka seperti itu memaksa orang doubleuntuk masuk ke "rentang manis" antara 2 ^ 52 dan 2 ^ 53, yang, seperti dijelaskan oleh Wikipedia di sini , memiliki properti yang menarik:

Antara 2 52 = 4,503.599.627.370.496 dan 2 53 = 9.007.199.254.740.992 angka yang dapat diwakili adalah bilangan bulat

Ini mengikuti dari fakta bahwa mantissa adalah lebar 52 bit.

Fakta menarik lainnya tentang menambahkan 2 51 +2 52 adalah bahwa ia mempengaruhi mantissa hanya dalam dua bit tertinggi - yang tetap dibuang, karena kami hanya mengambil 32 bit terendahnya.


Terakhir, tanda.

IEEE 754 floating point menggunakan representasi tanda dan magnitudo, sedangkan bilangan bulat pada mesin "normal" menggunakan aritmatika komplemen 2's; bagaimana ini ditangani di sini?

Kami hanya berbicara tentang bilangan bulat positif; sekarang anggaplah kita berhadapan dengan angka negatif dalam kisaran yang diwakili oleh 32-bit int, jadi lebih sedikit (dalam nilai absolut) daripada (-2 ^ 31 + 1); sebut itu -a. Angka seperti itu jelas dibuat positif dengan menambahkan angka ajaib, dan nilai yang dihasilkan adalah 2 52 +2 51 + (- a).

Sekarang, apa yang kita dapatkan jika kita menafsirkan mantissa dalam representasi komplemen 2? Itu harus hasil dari jumlah komplemen 2's (2 52 +2 51 ) dan (-a). Sekali lagi, istilah pertama hanya mempengaruhi dua bit bagian atas, yang tersisa dalam bit 0 ~ 50 adalah representasi komplemen 2 dari (-a) (sekali lagi, minus dua bit bagian atas).

Karena pengurangan jumlah komplemen 2 menjadi lebar lebih kecil dilakukan hanya dengan memotong bit tambahan di sebelah kiri, mengambil 32 bit yang lebih rendah memberi kita dengan benar (-a) dalam 32 bit, aritmatika komplemen 2's.

Matteo Italia
sumber
"" "Fakta menarik lainnya tentang menambahkan 2 ^ 51 + 2 ^ 52 adalah bahwa itu mempengaruhi mantissa hanya di dua bit tertinggi - yang dibuang, karena kita hanya mengambil 32 bit terendahnya" "" Apa itu? Menambahkan ini dapat mengubah semua mantissa!
YvesgereY
@ John: tentu saja, inti dari menambahkan mereka adalah untuk memaksa nilai berada dalam kisaran itu, yang jelas dapat mengakibatkan pergeseran mantissa (antara hal-hal lain) sehubungan dengan nilai aslinya. Apa yang saya katakan di sini adalah bahwa, begitu Anda berada dalam kisaran itu, satu-satunya bit yang berbeda dari integer 53 bit yang sesuai adalah bit 51 dan 52, yang tetap dibuang.
Matteo Italia
2
Bagi mereka yang ingin mengonversi ke int64_tAnda dapat melakukannya dengan menggeser mantissa ke kiri dan kemudian ke kanan dengan 13 bit. Ini akan menghapus eksponen dan dua bit dari angka 'ajaib', tetapi akan menyimpan dan menyebarkan tanda ke seluruh integer bertanda 64-bit. union { double d; int64_t l; } magic; magic.d = input + 6755399441055744.0; magic.l <<= 13; magic.l >>= 13;
Wojciech Migda