Mengapa ini sedikit kode,
const float x[16] = { 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8,
1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6};
const float z[16] = {1.123, 1.234, 1.345, 156.467, 1.578, 1.689, 1.790, 1.812,
1.923, 2.034, 2.145, 2.256, 2.367, 2.478, 2.589, 2.690};
float y[16];
for (int i = 0; i < 16; i++)
{
y[i] = x[i];
}
for (int j = 0; j < 9000000; j++)
{
for (int i = 0; i < 16; i++)
{
y[i] *= x[i];
y[i] /= z[i];
y[i] = y[i] + 0.1f; // <--
y[i] = y[i] - 0.1f; // <--
}
}
jalankan lebih dari 10 kali lebih cepat dari bit berikut (identik kecuali di mana dicatat)?
const float x[16] = { 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8,
1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6};
const float z[16] = {1.123, 1.234, 1.345, 156.467, 1.578, 1.689, 1.790, 1.812,
1.923, 2.034, 2.145, 2.256, 2.367, 2.478, 2.589, 2.690};
float y[16];
for (int i = 0; i < 16; i++)
{
y[i] = x[i];
}
for (int j = 0; j < 9000000; j++)
{
for (int i = 0; i < 16; i++)
{
y[i] *= x[i];
y[i] /= z[i];
y[i] = y[i] + 0; // <--
y[i] = y[i] - 0; // <--
}
}
saat kompilasi dengan Visual Studio 2010 SP1. Tingkat optimasi adalah -02
dengan sse2
aktif. Saya belum menguji dengan kompiler lain.
0
,0f
,0d
, atau bahkan(int)0
dalam konteks di manadouble
diperlukan.Jawaban:
Selamat datang di dunia floating-point yang didenormalkan ! Mereka dapat mendatangkan malapetaka pada kinerja !!!
Angka yang tidak normal (atau subnormal) adalah jenis peretasan untuk mendapatkan beberapa nilai tambahan yang mendekati nol dari representasi floating point. Operasi pada floating-point terdenormalkan bisa puluhan hingga ratusan kali lebih lambat daripada pada floating-point normal. Ini karena banyak prosesor tidak dapat menanganinya secara langsung dan harus menjebak dan menyelesaikannya menggunakan mikrokode.
Jika Anda mencetak angka-angka setelah 10.000 iterasi, Anda akan melihat bahwa mereka telah konvergen ke nilai yang berbeda tergantung pada apakah
0
atau0.1
digunakan.Berikut kode tes yang dikompilasi di x64:
Keluaran:
Perhatikan bagaimana pada putaran kedua jumlahnya sangat mendekati nol.
Angka yang didenormalkan umumnya jarang dan karenanya kebanyakan prosesor tidak mencoba menanganinya secara efisien.
Untuk menunjukkan bahwa ini semua ada hubungannya dengan angka-angka yang didenormalkan, jika kita membesar-besarkan denormals ke nol dengan menambahkan ini ke awal kode:
Kemudian versi dengan
0
tidak lagi 10x lebih lambat dan sebenarnya menjadi lebih cepat. (Ini mengharuskan kode dikompilasi dengan SSE diaktifkan.)Ini berarti bahwa alih-alih menggunakan nilai presisi hampir nol yang aneh ini, kami hanya membulatkannya ke nol.
Pengaturan waktu: Core i7 920 @ 3.5 GHz:
Pada akhirnya, ini benar-benar tidak ada hubungannya dengan apakah itu bilangan bulat atau floating-point. The
0
atau0.1f
diubah / disimpan menjadi luar daftar dari kedua loop. Sehingga tidak berpengaruh pada kinerja.sumber
+ 0.0f
dioptimalkan. Jika saya harus menebak, bisa jadi itu+ 0.0f
akan memiliki efek samping jikay[i]
kebetulan menjadi sinyalNaN
atau sesuatu ... Tapi saya bisa salah.Menggunakan
gcc
dan menerapkan diff pada perakitan yang dihasilkan hanya menghasilkan perbedaan ini:Yang
cvtsi2ssq
10 kali lebih lambat memang.Rupanya,
float
versi menggunakan register XMM yang dimuat dari memori, sementaraint
versi mengkonversiint
nilai nyata 0 untukfloat
menggunakancvtsi2ssq
instruksi, mengambil banyak waktu. Melewati-O3
ke gcc tidak membantu. (gcc versi 4.2.1.)(Menggunakan
double
bukannyafloat
tidak masalah, kecuali bahwa itu mengubahcvtsi2ssq
acvtsi2sdq
.)Memperbarui
Beberapa tes tambahan menunjukkan bahwa itu belum tentu
cvtsi2ssq
instruksi. Setelah dihilangkan (menggunakan aint ai=0;float a=ai;
dan menggunakana
bukan0
), perbedaan kecepatan tetap. Jadi @Mysticial benar, float denormalized membuat perbedaan. Ini dapat dilihat dengan menguji nilai antara0
dan0.1f
. Titik balik dalam kode di atas kira-kira di0.00000000000000000000000000000001
, ketika loop tiba-tiba memakan waktu 10 kali lebih lama.Perbarui << 1
Visualisasi kecil dari fenomena menarik ini:
Anda dapat dengan jelas melihat eksponen (9 bit terakhir) berubah ke nilai terendahnya, ketika denormalization masuk. Pada titik itu, penambahan sederhana menjadi 20 kali lebih lambat.
Diskusi yang setara tentang ARM dapat ditemukan di pertanyaan Stack Overflow. Titik mengambang yang dinormalisasi di Objective-C? .
sumber
-O
Ini tidak memperbaikinya, tetapi-ffast-math
tidak. (Saya menggunakannya sepanjang waktu, IMO kasus sudut di mana hal itu menyebabkan masalah presisi seharusnya tidak muncul dalam program yang dirancang dengan baik.)-ffast-math
tautan beberapa kode startup tambahan yang menetapkan FTZ (flush ke nol) dan DAZ (tidak normal adalah nol) di MXCSR, sehingga CPU tidak perlu lagi mengambil mikrokode bantu yang lambat untuk penyangkalan.Ini karena penggunaan floating-point yang didenormalkan. Bagaimana cara menyingkirkannya dan penalti kinerja? Setelah menjelajahi Internet untuk mencari cara membunuh nomor yang tidak normal, tampaknya belum ada cara "terbaik" untuk melakukan ini. Saya telah menemukan tiga metode ini yang paling berhasil di lingkungan yang berbeda:
Mungkin tidak berfungsi di beberapa lingkungan GCC:
Mungkin tidak berfungsi di beberapa lingkungan Visual Studio: 1
Tampaknya berfungsi di GCC dan Visual Studio:
Kompiler Intel memiliki opsi untuk menonaktifkan denormals secara default pada CPU Intel modern. Lebih detail di sini
Saklar kompiler.
-ffast-math
,-msse
atau-mfpmath=sse
akan menonaktifkan denormals dan membuat beberapa hal lain lebih cepat, tetapi sayangnya juga melakukan banyak perkiraan lain yang dapat merusak kode Anda. Uji dengan cermat! Setara dengan matematika cepat untuk kompiler Visual Studio adalah/fp:fast
tetapi saya belum dapat mengkonfirmasi apakah ini juga menonaktifkan denormals. 1sumber
Di gcc, Anda dapat mengaktifkan FTZ dan DAZ dengan ini:
juga gunakan sakelar gcc: -msse -mfpmath = sse
(kredit yang sesuai untuk Carl Hetherington [1])
[1] http://carlh.net/plugins/denormals.php
sumber
fesetround()
darifenv.h
(ditetapkan untuk C99) untuk yang lain, cara yang lebih portabel pembulatan ( linux.die.net/man/3/fesetround ) (tapi ini akan mempengaruhi semua operasi FP, bukan hanya subnormals )Komentar Dan Neely harus diperluas menjadi jawaban:
Bukan konstanta nol
0.0f
yang didenormalisasi atau menyebabkan pelambatan, melainkan nilai-nilai yang mendekati nol setiap iterasi dari loop. Ketika mereka semakin dekat dan semakin dekat ke nol, mereka membutuhkan lebih banyak ketelitian untuk mewakili dan mereka menjadi terdenormalisasi. Inilahy[i]
nilainya. (Mereka mendekati nol karenax[i]/z[i]
kurang dari 1,0 untuk semuai
.)Perbedaan penting antara versi kode yang lambat dan cepat adalah pernyataannya
y[i] = y[i] + 0.1f;
. Segera setelah garis ini dieksekusi setiap iterasi loop, presisi ekstra dalam float hilang, dan denormalization diperlukan untuk menyatakan bahwa presisi tidak lagi diperlukan. Setelah itu, operasi floating pointy[i]
tetap cepat karena tidak didenormalisasi.Mengapa presisi ekstra hilang saat Anda menambahkan
0.1f
? Karena angka floating point hanya memiliki begitu banyak digit signifikan. Katakanlah Anda memiliki cukup penyimpanan untuk tiga digit signifikan, lalu0.00001 = 1e-5
, dan0.00001 + 0.1 = 0.1
, setidaknya untuk format float contoh ini, karena tidak memiliki ruang untuk menyimpan bit yang paling tidak signifikan0.10001
.Singkatnya,
y[i]=y[i]+0.1f; y[i]=y[i]-0.1f;
bukan no-op yang mungkin Anda pikirkan.Mistik juga mengatakan ini : isi masalah mengapung, bukan hanya kode perakitan.
sumber