Saya memperhatikan hal yang aneh di komputer saya. * Tes keterbagian tulisan tangan secara signifikan lebih cepat daripada %
operator. Pertimbangkan contoh minimal:
* AMD Ryzen Threadripper 2990WX, GCC 9.2.0
static int divisible_ui_p(unsigned int m, unsigned int a)
{
if (m <= a) {
if (m == a) {
return 1;
}
return 0;
}
m += a;
m >>= __builtin_ctz(m);
return divisible_ui_p(m, a);
}
Contoh dibatasi oleh ganjil a
dan m > 0
. Namun, dapat dengan mudah digeneralisasikan untuk semua a
dan m
. Kode hanya mengubah divisi menjadi serangkaian tambahan.
Sekarang pertimbangkan program pengujian yang disusun dengan -std=c99 -march=native -O3
:
for (unsigned int a = 1; a < 100000; a += 2) {
for (unsigned int m = 1; m < 100000; m += 1) {
#if 1
volatile int r = divisible_ui_p(m, a);
#else
volatile int r = (m % a == 0);
#endif
}
}
... dan hasilnya di komputer saya:
| implementation | time [secs] |
|--------------------|-------------|
| divisible_ui_p | 8.52user |
| builtin % operator | 17.61user |
Karena itu lebih dari 2 kali lebih cepat.
Pertanyaannya: Bisakah Anda memberi tahu saya bagaimana kode berperilaku pada mesin Anda? Apakah itu kehilangan peluang pengoptimalan di GCC? Bisakah Anda melakukan tes ini lebih cepat?
PEMBARUAN: Seperti yang diminta, berikut adalah contoh minimal yang dapat direproduksi:
#include <assert.h>
static int divisible_ui_p(unsigned int m, unsigned int a)
{
if (m <= a) {
if (m == a) {
return 1;
}
return 0;
}
m += a;
m >>= __builtin_ctz(m);
return divisible_ui_p(m, a);
}
int main()
{
for (unsigned int a = 1; a < 100000; a += 2) {
for (unsigned int m = 1; m < 100000; m += 1) {
assert(divisible_ui_p(m, a) == (m % a == 0));
#if 1
volatile int r = divisible_ui_p(m, a);
#else
volatile int r = (m % a == 0);
#endif
}
}
return 0;
}
dikompilasi dengan gcc -std=c99 -march=native -O3 -DNDEBUG
AMD Ryzen Threadripper 2990WX dengan
gcc --version
gcc (Gentoo 9.2.0-r2 p3) 9.2.0
UPDATE2: Seperti yang diminta, versi yang dapat menangani apa saja a
dan m
(jika Anda juga ingin menghindari overflow integer, tes harus dilaksanakan dengan tipe integer dua kali selama input integer):
int divisible_ui_p(unsigned int m, unsigned int a)
{
#if 1
/* handles even a */
int alpha = __builtin_ctz(a);
if (alpha) {
if (__builtin_ctz(m) < alpha) {
return 0;
}
a >>= alpha;
}
#endif
while (m > a) {
m += a;
m >>= __builtin_ctz(m);
}
if (m == a) {
return 1;
}
#if 1
/* ensures that 0 is divisible by anything */
if (m == 0) {
return 1;
}
#endif
return 0;
}
r
s yang Anda hitung memang sama satu sama lain.a % b
memilikib
jauh lebih kecil daripadaa
. Melalui sebagian besar iterasi dalam test case Anda, mereka memiliki ukuran yang sama, ataub
lebih besar, dan versi Anda bisa lebih cepat pada banyak CPU dalam situasi tersebut.Jawaban:
Apa yang Anda lakukan disebut pengurangan kekuatan: mengganti operasi yang mahal dengan serangkaian operasi yang murah.
Instruksi mod pada banyak CPU lambat, karena secara historis tidak diuji dalam beberapa tolok ukur umum dan oleh karena itu perancang mengoptimalkan instruksi lain sebagai gantinya. Algoritma ini akan berkinerja lebih buruk jika harus melakukan banyak iterasi, dan
%
akan berkinerja lebih baik pada CPU di mana ia hanya membutuhkan dua siklus clock.Akhirnya, ketahuilah bahwa ada banyak jalan pintas untuk mengambil sisa pembagian oleh konstanta tertentu. (Meskipun kompiler umumnya akan mengurus ini untuk Anda.)
sumber
div
/idiv
yang mendapatkan kecintaan pada Intel Penryn, Broadwell dan IceLake (pembagi perangkat keras radix yang lebih tinggi)x = i * const
setiap iterasi yang Anda lakukanx += const
setiap iterasi. Saya tidak berpikir mengganti perkalian tunggal dengan shift / add loop akan disebut pengurangan kekuatan. en.wikipedia.org/wiki/… mengatakan istilah ini mungkin dapat digunakan dengan cara ini, tetapi dengan catatan "Materi ini diperdebatkan. Lebih baik digambarkan sebagai optimasi lubang dan penugasan instruksi."Saya akan menjawab pertanyaan saya sendiri. Tampaknya saya menjadi korban prediksi cabang. Ukuran timbal-balik dari operan tampaknya tidak penting, hanya pesanan mereka.
Pertimbangkan implementasi berikut
dan array
yang sedang / tidak dikocok menggunakan fungsi acak .
Tanpa pengocokan, hasilnya tetap
Namun, setelah saya mengocok array ini, hasilnya berbeda
sumber