Mengapa Dentang mengoptimalkan jauh x * 1.0 tetapi TIDAK x + 0.0?

125

Mengapa Dentang mengoptimalkan pengulangan dalam kode ini

#include <time.h>
#include <stdio.h>

static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };

int main()
{
    clock_t const start = clock();
    for (int i = 0; i < N; ++i) { arr[i] *= 1.0; }
    printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}

tetapi bukan loop dalam kode ini?

#include <time.h>
#include <stdio.h>

static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };

int main()
{
    clock_t const start = clock();
    for (int i = 0; i < N; ++i) { arr[i] += 0.0; }
    printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}

(Menandai C dan C ++ karena saya ingin tahu apakah jawabannya berbeda untuk masing-masing.)

pengguna541686
sumber
2
Bendera optimasi mana yang saat ini aktif?
Iwillnotexist Idonotexist
1
@IwillnotexistIdonotexist: Saya baru saja menggunakan -O3, saya tidak tahu bagaimana memeriksa apa yang diaktifkan.
user541686
2
Akan menarik untuk melihat apa yang terjadi jika Anda menambahkan -fast-matematika ke baris perintah.
plugwash
static double arr[N]tidak diizinkan dalam C; constvariabel tidak dihitung sebagai ekspresi konstan dalam bahasa itu
MM
1
[Masukkan komentar snarky tentang bagaimana C bukan C ++, meskipun Anda sudah memanggilnya.]
user253751

Jawaban:

164

Standar IEEE 754-2008 untuk Floating-Point Arithmetic dan ISO / IEC 10967 Language Independent Arithmetic (LIA) Standard, Bagian 1 menjawab mengapa demikian.

IEEE 754 § 6.3 Tanda itu sedikit

Ketika input atau hasil adalah NaN, standar ini tidak menafsirkan tanda NaN. Perhatikan, bagaimanapun, bahwa operasi pada string bit - copy, negate, abs, copySign - tentukan bit tanda dari hasil NaN, kadang-kadang berdasarkan bit tanda operan NaN. TotalOrder predikat logis juga dipengaruhi oleh bit tanda operan NaN. Untuk semua operasi lain, standar ini tidak menentukan bit tanda hasil NaN, bahkan ketika hanya ada satu input NaN, atau ketika NaN dihasilkan dari operasi yang tidak valid.

Ketika input atau hasil bukan NaN, tanda suatu produk atau hasil bagi adalah OR eksklusif atau dari tanda operan; tanda penjumlahan, atau perbedaan x - y dianggap sebagai penjumlahan x + (−y), berbeda dari paling banyak salah satu dari tanda-tanda tambahan; dan tanda hasil konversi, operasi kuantisasi, operasi roundTo-Integral, dan roundToIntegralExact (lihat 5.3.1) adalah tanda operan pertama atau satu-satunya. Aturan-aturan ini akan berlaku bahkan ketika operan atau hasilnya nol atau tidak terbatas.

Ketika jumlah dua operan dengan tanda-tanda yang berlawanan (atau perbedaan dari dua operan dengan tanda-tanda serupa) adalah nol, tanda dari jumlah (atau perbedaan) tersebut harus +0 dalam semua atribut arah pembulatan kecuali roundTowardNegative; di bawah atribut itu, tanda jumlah nol yang tepat (atau perbedaan) harus −0. Namun, x + x = x - (−x) mempertahankan tanda yang sama dengan x bahkan ketika x adalah nol.

Kasus Penambahan

Di bawah mode pembulatan standar (Round-to-Nearest, Ties-to-Even) , kita melihat bahwa x+0.0menghasilkan x, KECUALI ketika xadalah -0.0: Dalam hal itu kita memiliki jumlah dua operan dengan tanda-tanda berlawanan yang jumlahnya adalah nol, dan §6,3 paragraf 3 aturan yang dihasilkan tambahan ini +0.0.

Karena bitwise+0.0 tidak identik dengan aslinya , dan itu adalah nilai yang sah yang dapat terjadi sebagai input, kompiler wajib memasukkan kode yang akan mengubah nol potensial menjadi negatif .-0.0-0.0+0.0

Ringkasan: Di bawah mode pembulatan default, dalam x+0.0, jikax

  • tidak -0.0 , maka xitu sendiri adalah nilai output yang dapat diterima.
  • adalah -0.0 , maka nilai output harus +0.0 , yang tidak identik dengan bitwise -0.0.

Kasus Multiplikasi

Di bawah mode pembulatan default , tidak ada masalah seperti itu terjadi x*1.0. Jika x:

  • adalah (normal) angka normal, x*1.0 == xselalu.
  • adalah +/- infinity, maka hasilnya adalah +/- infinitydari tanda yang sama.
  • adalah NaN, maka menurut

    IEEE 754 § 6.2.3 Propagasi NaN

    Suatu operasi yang menyebarkan operan NaN ke hasilnya dan memiliki NaN tunggal sebagai input harus menghasilkan NaN dengan muatan input NaN jika mewakili dalam format tujuan.

    yang berarti bahwa eksponen dan mantissa (meskipun tidak tanda) dari NaN*1.0yang dianjurkan tidak berubah dari input NaN. Tanda tidak ditentukan sesuai dengan §6.3p1 di atas, tetapi implementasi dapat menetapkannya identik dengan sumber NaN.

  • adalah +/- 0.0, maka hasilnya adalah 0dengan bit tanda XOR dengan bit tanda 1.0, sesuai dengan §6.3p2. Karena bit tanda 1.0adalah 0, nilai output tidak berubah dari input. Jadi, x*1.0 == xbahkan ketika xnol (negatif).

Kasus Pengurangan

Di bawah mode pembulatan default , pengurangan x-0.0juga merupakan larangan, karena setara dengan x + (-0.0). Jika xada

  • adalah NaN, maka §6.3p1 dan §6.2.3 berlaku dengan cara yang sama seperti untuk penambahan dan penggandaan.
  • adalah +/- infinity, maka hasilnya adalah +/- infinitydari tanda yang sama.
  • adalah (normal) angka normal, x-0.0 == xselalu.
  • adalah -0.0, maka oleh §6.3p2 kita memiliki " [...] tanda penjumlahan, atau perbedaan x - y dianggap sebagai penjumlahan x + (fromy), berbeda dari paling banyak salah satu dari tanda tambahan; ". Ini memaksa kita untuk menetapkan -0.0sebagai hasil dari (-0.0) + (-0.0), karena -0.0berbeda dalam tanda dari tidak ada tambahan, sementara +0.0berbeda dalam tanda dari dua dari tambahan, yang melanggar pasal ini.
  • adalah +0.0, maka ini mengurangi kasus penambahan yang (+0.0) + (-0.0)dipertimbangkan di atas dalam Kasus Penambahan , yang oleh §6.3p3 diperintahkan untuk diberikan +0.0.

Karena untuk semua kasus, nilai input legal sebagai output, diizinkan untuk mempertimbangkan x-0.0larangan, dan x == x-0.0tautologi.

Optimalisasi Perubahan Nilai

Standar IEEE 754-2008 memiliki kutipan menarik berikut:

IEEE 754 § 10.4 Arti literal dan optimisasi yang mengubah nilai

[...]

Transformasi yang mengubah nilai berikut, antara lain, mempertahankan makna literal kode sumber:

  • Menerapkan properti identitas 0 + x ketika x bukan nol dan bukan NaN pensinyalan dan hasilnya memiliki eksponen yang sama dengan x.
  • Menerapkan properti identitas 1 × x ketika x bukan NaN pensinyalan dan hasilnya memiliki eksponen yang sama dengan x.
  • Mengubah muatan atau masuk sedikit NaN yang tenang.
  • [...]

Karena semua NaN dan semua infinities berbagi eksponen yang sama, dan hasil benar bulat x+0.0dan x*1.0untuk yang terbatas xmemiliki tepat besarnya sama x, eksponen mereka adalah sama.

sNaNs

Signalling NaNs adalah nilai perangkap floating-point; Mereka adalah nilai NaN khusus yang digunakan sebagai operan titik-mengambang menghasilkan pengecualian operasi tidak valid (SIGFPE). Jika loop yang memicu pengecualian dioptimalkan, perangkat lunak tidak akan lagi berperilaku sama.

Namun, seperti yang ditunjukkan oleh user2357112 dalam komentar , Standar C11 secara eksplisit membiarkan perilaku pensinyalan NaNs ( sNaN), sehingga penyusun diizinkan untuk menganggap mereka tidak terjadi, dan dengan demikian pengecualian yang mereka ajukan juga tidak terjadi. C ++ 11 menghilangkan standar yang menggambarkan perilaku untuk memberi sinyal NaN, dan dengan demikian juga membiarkannya tidak terdefinisi.

Mode Pembulatan

Dalam mode pembulatan alternatif, optimisasi yang diizinkan dapat berubah. Misalnya, dalam mode Round-to-Negative-Infinity , optimasi x+0.0 -> xmenjadi diizinkan, tetapi x-0.0 -> xmenjadi terlarang.

Untuk mencegah GCC dari mengasumsikan mode dan perilaku pembulatan default, bendera eksperimental -frounding-mathdapat diteruskan ke GCC.

Kesimpulan

Dentang dan GCC , bahkan pada -O3, tetap sesuai IEEE-754. Ini berarti harus mematuhi aturan standar IEEE-754 di atas. x+0.0adalah tidak bit-identik untuk xsemua xdi bawah aturan-aturan, tetapi x*1.0 dapat dipilih untuk menjadi begitu : Yaitu, ketika kita

  1. Patuhi rekomendasi untuk meneruskan payload yang tidak berubah xketika itu adalah NaN.
  2. Biarkan sedikit tanda hasil NaN tidak diubah oleh * 1.0.
  3. Mematuhi perintah untuk XOR tanda sedikit selama quotient / produk, saat xini tidak NaN.

Untuk mengaktifkan pengoptimalan yang tidak aman dari IEEE-754 (x+0.0) -> x, flag tersebut -ffast-mathharus diteruskan ke Dentang atau GCC.

Iwillnotexist Idonotexist
sumber
2
Peringatan: bagaimana jika itu adalah sinyal NaN? (Saya benar-benar berpikir itu mungkin menjadi alasannya, tapi saya tidak benar-benar tahu caranya, jadi saya bertanya.)
user541686
6
@Mehrdad: Lampiran F, bagian (opsional) dari standar C yang menentukan kepatuhan C terhadap IEEE 754, secara eksplisit tidak mencakup pensinyalan NaN. (C11 F.2.1., Baris pertama: "Spesifikasi ini tidak mendefinisikan perilaku pensinyalan NaN.") Implementasi yang menyatakan kesesuaian dengan Lampiran F tetap bebas untuk melakukan apa yang mereka inginkan dengan pensinyalan NaN. Standar C ++ memiliki penanganan sendiri IEEE 754, tapi apa pun itu (saya tidak terbiasa), saya ragu itu menentukan perilaku pensinyalan NaN juga.
user2357112 mendukung Monica
2
@Mehrdad: sNaN memanggil perilaku yang tidak terdefinisi sesuai dengan standar (tapi mungkin didefinisikan dengan baik oleh platform) sehingga kompilator squashing di sini diperbolehkan.
Yosua
1
@ user2357112: Kemungkinan jebakan kesalahan sebagai efek samping untuk kalkulasi yang tidak digunakan biasanya mengganggu banyak optimasi; jika hasil perhitungan terkadang diabaikan, kompiler mungkin akan menunda perhitungan sampai dapat mengetahui apakah hasilnya akan digunakan, tetapi jika perhitungan akan menghasilkan sinyal penting, itu bisa menjadi buruk.
supercat
2
Oh, lihat, sebuah pertanyaan yang secara sah berlaku untuk C dan C ++ yang dijawab secara akurat untuk kedua bahasa dengan mengacu pada standar tunggal. Apakah ini akan membuat orang cenderung tidak mengeluh tentang pertanyaan yang ditandai dengan C dan C ++, bahkan ketika pertanyaan tersebut berhubungan dengan kesamaan bahasa? Sayangnya, saya pikir tidak.
Kyle Strand
35

x += 0.0bukan NOOP jika xada -0.0. Pengoptimal tetap bisa menghapus seluruh loop karena hasilnya tidak digunakan. Secara umum, sulit untuk mengatakan mengapa pengoptimal membuat keputusan.

user2357112 mendukung Monica
sumber
2
Saya sebenarnya memposting ini setelah saya baru saja membaca mengapa x += 0.0bukan no-op, namun saya pikir itu mungkin bukan alasan karena seluruh loop harus dioptimalkan dengan cara baik. Saya dapat membelinya, hanya saja tidak sepenuhnya meyakinkan seperti yang saya harapkan ...
user541686
Mengingat kecenderungan bahasa berorientasi objek untuk menghasilkan efek samping, saya akan membayangkan bahwa akan sulit untuk memastikan bahwa pengoptimal tidak mengubah perilaku aktual.
Robert Harvey
Bisa jadi alasannya, karena dengan long longoptimasi berlaku (melakukannya dengan gcc, yang berperilaku sama untuk setidaknya dua kali lipat )
e2-e4
2
@ ringø: long longadalah tipe integral, bukan tipe IEEE754.
MSalters
1
Bagaimana x -= 0, apakah sama?
Viktor Mellgren