Mengapa kompilator tidak dapat mengoptimalkan loop penambahan yang dapat diprediksi menjadi perkalian?


133

Ini adalah pertanyaan yang muncul di benak saya ketika membaca jawaban brilian oleh Mysticial untuk pertanyaan: mengapa lebih cepat memproses array yang diurutkan daripada array yang tidak diurutkan ?

Konteks untuk jenis yang terlibat:

const unsigned arraySize = 32768;
int data[arraySize];
long long sum = 0;

Dalam jawabannya dia menjelaskan bahwa Intel Compiler (ICC) mengoptimalkan ini:

for (int i = 0; i < 100000; ++i)
    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            sum += data[c];

... menjadi sesuatu yang setara dengan ini:

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

Pengoptimal mengakui bahwa ini adalah setara dan karenanya bertukar loop , memindahkan cabang di luar loop dalam. Sangat pintar!

Tetapi mengapa tidak melakukan ini?

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000 * data[c];

Semoga Mysticial (atau siapa pun) dapat memberikan jawaban yang sama-sama brilian. Saya belum pernah belajar tentang optimasi yang dibahas dalam pertanyaan lain sebelumnya, jadi saya sangat berterima kasih untuk ini.


14
Itu adalah sesuatu yang mungkin hanya diketahui oleh Intel. Saya tidak tahu urutan apa yang dijalankan melalui pengoptimalannya. Dan ternyata, ia tidak menjalankan loop-collapsing pass setelah loop-interchange.
Mysticial

7
Optimalisasi ini hanya valid jika nilai yang terkandung dalam array data tidak dapat diubah. Misalnya, jika memori dipetakan ke perangkat input / output setiap kali Anda membaca data [0] akan menghasilkan nilai yang berbeda ...
Thomas CG de Vilhena

2
Tipe data apa ini, integer atau floating-point? Penambahan berulang dalam floating-point memberikan hasil yang sangat berbeda dari perkalian.
Ben Voigt

6
@ Thomas: Jika data itu volatile, maka pertukaran loop akan menjadi optimasi yang tidak valid juga.
Ben Voigt

3
GNAT (Ada compiler dengan GCC 4.6) tidak akan mengganti loop di O3, tetapi jika loop diaktifkan, itu akan mengubahnya menjadi multiplikasi.
prosfilaes

Jawaban:


105

Kompilator umumnya tidak dapat mentransformasikannya

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

ke

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000 * data[c];

karena yang terakhir dapat menyebabkan overflow bilangan bulat yang ditandatangani di mana yang pertama tidak. Bahkan dengan perilaku wrap-around yang dijamin untuk overflow bilangan bulat pelengkap dua yang ditandatangani, itu akan mengubah hasilnya (jika data[c]30000, produk akan menjadi -1294967296untuk 32-bit khas intdengan lilitan sekitar, sementara 100000 kali menambahkan 30000 sumakan, jika itu tidak meluap, naik sum3000000000). Perhatikan bahwa hal yang sama berlaku untuk jumlah yang tidak ditandatangani, dengan angka yang berbeda, luapan dari 100000 * data[c]biasanya akan memperkenalkan modulo reduksi 2^32yang tidak boleh muncul dalam hasil akhir.

Itu bisa mengubahnya menjadi

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000LL * data[c];  // resp. 100000ull

meskipun, jika, seperti biasa, long longcukup besar int.

Mengapa tidak melakukan itu, saya tidak tahu, saya kira itulah yang dikatakan Mysticial , "tampaknya, ia tidak menjalankan loop-collapsing setelah loop-interchange".

Perhatikan bahwa loop-interchange itu sendiri umumnya tidak valid (untuk bilangan bulat yang ditandatangani), karena

for (int c = 0; c < arraySize; ++c)
    if (condition(data[c]))
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

dapat menyebabkan meluapnya tempat

for (int i = 0; i < 100000; ++i)
    for (int c = 0; c < arraySize; ++c)
        if (condition(data[c]))
            sum += data[c];

tidak akan. Ini halal di sini, karena kondisi memastikan semua data[c]yang ditambahkan memiliki tanda yang sama, jadi jika satu meluap, keduanya melakukannya.

Saya tidak akan terlalu yakin bahwa kompiler memperhitungkannya (@Mysticial, dapatkah Anda mencoba dengan kondisi seperti data[c] & 0x80atau sehingga bisa benar untuk nilai positif dan negatif?). Saya memiliki kompiler membuat optimisasi yang tidak valid (misalnya, beberapa tahun yang lalu, saya memiliki ICC (11.0, iirc) menggunakan konversi ditandatangani-32-bit-int-ke-ganda di 1.0/nmana nadalah unsigned int. Sekitar dua kali lebih cepat dari gcc output. Tapi salah, banyak nilai lebih besar dari 2^31, oops.).


4
Saya ingat versi kompiler MPW yang menambahkan opsi untuk memungkinkan stack frame lebih besar dari 32K [versi sebelumnya dibatasi menggunakan @ A7 + int16 untuk variabel lokal]. Itu mendapat segalanya dengan benar untuk frame stack di bawah 32K atau lebih dari 64K, tetapi untuk frame stack 40K itu akan digunakan ADD.W A6,$A000, lupa bahwa operasi kata dengan register register sign-memperpanjang kata menjadi 32 bit sebelum menambahkan. Butuh beberapa saat untuk memecahkan masalah, karena satu-satunya hal yang dilakukan kode antara itu ADDdan waktu berikutnya muncul A6 dari tumpukan adalah untuk mengembalikan register pemanggil itu telah disimpan ke bingkai itu ...
supercat

3
... dan satu-satunya register yang dipedulikan si penelepon adalah alamat [load-time constant] dari array statis. Kompiler tahu bahwa alamat array disimpan dalam register sehingga dapat mengoptimalkan berdasarkan itu, tetapi debugger hanya tahu alamat konstanta. Jadi, sebelum pernyataan MyArray[0] = 4;saya bisa memeriksa alamat MyArray, dan melihat lokasi itu sebelum dan sesudah pernyataan dieksekusi; itu tidak akan berubah. Kode adalah sesuatu seperti move.B @A3,#4dan A3 seharusnya selalu menunjuk MyArraykapan saja instruksi dijalankan, tetapi tidak. Menyenangkan.
supercat

lalu mengapa dentang melakukan optimasi semacam ini?
Jason S

Kompiler dapat melakukan penulisan ulang itu dalam representasi perantara internal, karena itu memungkinkan untuk memiliki perilaku kurang terdefinisi dalam representasi perantara internal.
user253751

48

Jawaban ini tidak berlaku untuk kasus spesifik yang ditautkan, tetapi berlaku untuk judul pertanyaan dan mungkin menarik bagi pembaca di masa mendatang:

Karena presisi yang terbatas, penambahan floating-point tidak setara dengan perkalian . Mempertimbangkan:

float const step = 1e-15;
float const init = 1;
long int const count = 1000000000;

float result1 = init;
for( int i = 0; i < count; ++i ) result1 += step;

float result2 = init;
result2 += step * count;

cout << (result1 - result2);

Demo


10
Ini bukan jawaban untuk pertanyaan yang diajukan. Meskipun informasi menarik (dan harus tahu untuk programmer C / C ++), ini bukan forum, dan bukan milik di sini.
orlp

30
@nightcracker: Sasaran StackOverflow yang dinyatakan adalah untuk membangun perpustakaan jawaban yang dapat dicari yang berguna bagi pengguna di masa mendatang. Dan ini adalah jawaban untuk pertanyaan yang diajukan ... Kebetulan ada beberapa informasi yang tidak disebutkan yang membuat jawaban ini tidak berlaku untuk poster aslinya. Mungkin masih berlaku untuk orang lain dengan pertanyaan yang sama.
Ben Voigt

12
Itu bisa menjadi jawaban untuk judul pertanyaan , tapi bukan pertanyaannya, tidak.
orlp

7
Seperti yang saya katakan, ini adalah informasi yang menarik . Namun tampaknya masih salah bagi saya bahwa nota jawaban atas pertanyaan tidak menjawab pertanyaan seperti yang ada sekarang . Ini bukan alasan mengapa Intel Compiler memutuskan untuk tidak mengoptimalkan, basta.
orlp

4
@ nightcracker: Sepertinya salah bagi saya juga bahwa ini adalah jawaban teratas. Saya berharap seseorang memposting jawaban yang benar-benar bagus untuk kasus integer yang melampaui skor ini. Sayangnya, saya tidak berpikir ada jawaban untuk "tidak bisa" untuk kasus bilangan bulat, karena transformasi akan sah, jadi kita pergi dengan "mengapa tidak", yang sebenarnya bertentangan dengan " terlalu dekat "alasan dekat, karena ini khas untuk versi kompiler tertentu. Pertanyaan yang saya jawab adalah yang lebih penting, IMO.
Ben Voigt

6

Kompiler berisi berbagai lintasan yang melakukan optimasi. Biasanya di setiap pass baik optimasi pada pernyataan atau optimasi loop dilakukan. Saat ini tidak ada model yang melakukan optimalisasi loop body berdasarkan pada header loop. Ini sulit dideteksi dan kurang umum.

Optimasi yang dilakukan adalah gerakan kode invarian loop. Ini dapat dilakukan dengan menggunakan serangkaian teknik.


4

Yah, saya kira beberapa kompiler mungkin melakukan optimasi semacam ini, dengan asumsi bahwa kita berbicara tentang Integer Arithmetics.

Pada saat yang sama, beberapa kompiler mungkin menolak untuk melakukannya karena mengganti penambahan berulang dengan multiplikasi dapat mengubah perilaku overflow kode. Untuk tipe integer yang tidak ditandatangani, seharusnya tidak membuat perbedaan karena perilaku overflow mereka ditentukan sepenuhnya oleh bahasa. Tapi untuk yang sudah masuk, mungkin (mungkin tidak pada platform komplemen 2's). Memang benar bahwa limpahan yang ditandatangani sebenarnya mengarah ke perilaku yang tidak terdefinisi dalam C, yang berarti bahwa itu boleh saja untuk mengabaikan semantik melimpah itu sama sekali, tetapi tidak semua kompiler cukup berani untuk melakukan itu. Seringkali menuai banyak kritik dari kerumunan "C hanya bahasa tingkat tinggi". (Ingat apa yang terjadi ketika GCC memperkenalkan optimisasi berdasarkan semantik aliasing yang ketat?)

Secara historis, GCC telah menunjukkan dirinya sebagai kompiler yang memiliki apa yang diperlukan untuk mengambil langkah drastis seperti itu, tetapi kompiler lain mungkin lebih memilih untuk tetap dengan perilaku "yang dimaksudkan pengguna" yang dirasakan bahkan jika itu tidak ditentukan oleh bahasa.


Saya lebih suka mengetahui apakah saya secara tidak sengaja bergantung pada perilaku yang tidak terdefinisi, tapi saya kira kompiler tidak memiliki cara untuk mengetahui karena overflow akan menjadi masalah run-time: /
jhabbott

2
@jhabbott: jika terjadi overflow, maka ada perilaku yang tidak ditentukan. Apakah perilaku didefinisikan tidak diketahui sampai runtime (dengan asumsi angka-angka adalah input saat runtime): P.
orlp

3

Itu sekarang - setidaknya, dentang tidak :

long long add_100k_signed(int *data, int arraySize)
{
    long long sum = 0;

    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            for (int i = 0; i < 100000; ++i)
                sum += data[c];
    return sum;
}

kompilasi dengan -O1 ke

add_100k_signed:                        # @add_100k_signed
        test    esi, esi
        jle     .LBB0_1
        mov     r9d, esi
        xor     r8d, r8d
        xor     esi, esi
        xor     eax, eax
.LBB0_4:                                # =>This Inner Loop Header: Depth=1
        movsxd  rdx, dword ptr [rdi + 4*rsi]
        imul    rcx, rdx, 100000
        cmp     rdx, 127
        cmovle  rcx, r8
        add     rax, rcx
        add     rsi, 1
        cmp     r9, rsi
        jne     .LBB0_4
        ret
.LBB0_1:
        xor     eax, eax
        ret

Overflow integer tidak ada hubungannya dengan itu; jika ada integer overflow yang menyebabkan perilaku tidak terdefinisi, itu bisa terjadi dalam kedua kasus tersebut. Inilah jenis fungsi yang sama yang digunakan intalih-alihlong :

int add_100k_signed(int *data, int arraySize)
{
    int sum = 0;

    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            for (int i = 0; i < 100000; ++i)
                sum += data[c];
    return sum;
}

kompilasi dengan -O1 ke

add_100k_signed:                        # @add_100k_signed
        test    esi, esi
        jle     .LBB0_1
        mov     r9d, esi
        xor     r8d, r8d
        xor     esi, esi
        xor     eax, eax
.LBB0_4:                                # =>This Inner Loop Header: Depth=1
        mov     edx, dword ptr [rdi + 4*rsi]
        imul    ecx, edx, 100000
        cmp     edx, 127
        cmovle  ecx, r8d
        add     eax, ecx
        add     rsi, 1
        cmp     r9, rsi
        jne     .LBB0_4
        ret
.LBB0_1:
        xor     eax, eax
        ret

2

Ada hambatan konseptual untuk optimasi semacam ini. Penulis kompiler menghabiskan banyak upaya untuk pengurangan kekuatan - misalnya, mengganti perkalian dengan penambahan dan pergeseran. Mereka terbiasa berpikir bahwa perkalian itu buruk. Jadi kasus di mana seseorang harus pergi ke arah yang lain mengejutkan dan berlawanan dengan intuisi. Jadi tidak ada yang berpikir untuk mengimplementasikannya.


3
Mengganti loop dengan perhitungan form tertutup juga merupakan pengurangan kekuatan, bukan?
Ben Voigt

Secara formal, ya, saya kira, tetapi saya belum pernah mendengar ada yang membicarakannya seperti itu. (Namun, saya agak ketinggalan zaman pada literatur.)
zwol

1

Orang-orang yang mengembangkan dan memelihara kompiler memiliki jumlah waktu dan energi yang terbatas untuk dihabiskan pada pekerjaan mereka, sehingga mereka umumnya ingin fokus pada apa yang paling dipedulikan pengguna mereka: mengubah kode yang ditulis dengan baik menjadi kode cepat. Mereka tidak ingin menghabiskan waktu mencoba mencari cara untuk mengubah kode konyol menjadi kode cepat — itulah tujuan dari tinjauan kode. Dalam bahasa tingkat tinggi, mungkin ada kode "konyol" yang mengekspresikan ide penting, menjadikannya sepadan dengan waktu pengembang untuk membuatnya secepat itu — misalnya, penggundulan hutan jalan pintas dan penggabungan aliran memungkinkan program Haskell terstruktur di sekitar jenis malas tertentu menghasilkan struktur data untuk dikompilasi menjadi loop ketat yang tidak mengalokasikan memori. Tetapi insentif semacam itu tidak berlaku untuk mengubah penambahan yang berulang menjadi multiplikasi. Jika Anda ingin cepat,

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.