Di C ++, haruskah saya repot-repot melakukan cache variabel, atau membiarkan compiler melakukan optimasi? (Aliasing)

114

Pertimbangkan kode berikut ( padalah tipe unsigned char*dan bitmap->widthdari beberapa tipe integer, persis yang tidak diketahui dan bergantung pada versi mana dari beberapa perpustakaan eksternal yang kita gunakan):

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

Apakah layak untuk dioptimalkan [..]

Mungkinkah ada kasus di mana ini dapat menghasilkan hasil yang lebih efisien dengan menulis:

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

... atau apakah hal ini sepele untuk dioptimasi oleh compiler?

Apa yang Anda anggap sebagai kode yang "lebih baik"?

Catatan dari editor (Ike): bagi mereka yang bertanya-tanya tentang teks coretan, pertanyaan asli, seperti yang diutarakan, sangat dekat dengan wilayah di luar topik dan hampir ditutup meskipun ada tanggapan positif. Ini telah dilumpuhkan. Namun tolong jangan menghukum penjawab yang menjawab bagian pertanyaan yang terserang ini.

Yaron Cohen-Tal
sumber
19
Jika *pmemiliki tipe yang sama widthmaka tidak mudah untuk dioptimalkan, karena pbisa menunjuk widthdan memodifikasinya di dalam loop.
emlai
31
Bertanya tentang apakah kompilator mengoptimalkan operasi tertentu biasanya merupakan pertanyaan yang salah. Apa yang (biasanya) sangat Anda minati adalah versi mana yang berjalan lebih cepat, yang seharusnya Anda ukur.
SirGuy
4
@GuyGreer Saya setuju, meskipun menurut saya pertanyaannya bagus, atau setidaknya menarik, sayangnya jawabannya adalah "Anda harus mengukurnya, per kasus penggunaan". Alasannya adalah karena fungsinya portabel tetapi kinerjanya tidak. Jadi sebenarnya ini tergantung pada setiap bagian dari proses build, mulai dari kompiler dan berakhir di situs target (kombinasi os / hardware). Dan tentu saja tebakan terbaik adalah bahwa kompilernya lebih pintar daripada manusia dalam hal ini.
luk32
19
Jika saya adalah seorang kompiler, saya akan melihat bahwa dua contoh Anda tidak sama. Mungkin saja itu pmenunjuk ke memori yang sama dengan bitmap->width. Oleh karena itu saya tidak dapat secara legal mengoptimalkan contoh pertama ke yang kedua.
Mysticial
4
Di mana "p" disimpan? Saya menyarankan agar Anda mendapatkan kinerja yang sangat baik dengan melakukan sesuatu seperti "char * batasan p2 = p;" dan kemudian menggunakan "p2" daripada "p" dalam loop Anda. Kemudian, jika Anda ingin perubahan "p2" diterapkan kembali ke p, gunakan "p + = (p2-p);". Perhatikan bahwa tidak ada pointer yang ditulis dalam masa hidup p2 oleh pointer yang tidak disalin bentuk p2 dapat dibaca menggunakan pointer yang disalin dari p2, atau sebaliknya, dan tidak ada salinan p2 yang dapat digunakan untuk tujuan apa pun setelah masa pakai p2, tetapi kompiler dapat menggunakannya fakta untuk mengaktifkan pengoptimalan yang tidak dapat dicapai melalui cara lain.
supercat

Jawaban:

81

Pada pandangan pertama, saya pikir compiler dapat menghasilkan assembly yang setara untuk kedua versi dengan flag optimasi yang diaktifkan. Ketika saya memeriksanya, saya terkejut melihat hasilnya:

Sumber unoptimized.cpp

catatan: kode ini tidak dimaksudkan untuk dieksekusi.

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    for (unsigned x = 0 ; x < static_cast<unsigned>(bitmap.width) ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

Sumber optimized.cpp

catatan: kode ini tidak dimaksudkan untuk dieksekusi.

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    const unsigned width = static_cast<unsigned>(bitmap.width);
    for (unsigned x = 0 ; x < width ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

Kompilasi

  • $ g++ -s -O3 unoptimized.cpp
  • $ g++ -s -O3 optimized.cpp

Perakitan (tidak dioptimalkan)

    .file   "unoptimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    mov %eax, %edx
    addl    $1, %eax
    movq    (%rsi,%rdx,8), %rdx
    movb    $0, (%rdx)
    cmpl    bitmap(%rip), %eax
    jb  .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

Perakitan (dioptimalkan.)

    .file   "optimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    subl    $1, %eax
    leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    movq    (%rsi,%rax), %rdx
    addq    $8, %rax
    cmpq    %rcx, %rax
    movb    $0, (%rdx)
    jne .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

beda

$ diff -uN unoptimized.s optimized.s
--- unoptimized.s   2015-11-24 16:11:55.837922223 +0000
+++ optimized.s 2015-11-24 16:12:02.628922941 +0000
@@ -1,4 +1,4 @@
-   .file   "unoptimized.cpp"
+   .file   "optimized.cpp"
    .text
    .p2align 4,,15
 .globl main
@@ -10,16 +10,17 @@
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
+   subl    $1, %eax
+   leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
 .L3:
-   mov %eax, %edx
-   addl    $1, %eax
-   movq    (%rsi,%rdx,8), %rdx
+   movq    (%rsi,%rax), %rdx
+   addq    $8, %rax
+   cmpq    %rcx, %rax
    movb    $0, (%rdx)
-   cmpl    bitmap(%rip), %eax
-   jb  .L3
+   jne .L3
 .L2:
    xorl    %eax, %eax
    ret

Rakitan yang dihasilkan untuk versi yang dioptimalkan sebenarnya memuat ( lea) widthkonstanta tidak seperti versi yang tidak dioptimalkan yang menghitung widthoffset pada setiap iterasi ( movq).

Ketika saya punya waktu, saya akhirnya memposting beberapa patokan untuk itu. Pertanyaan bagus.

YSC
sumber
3
Akan menarik untuk melihat apakah kode dibuat secara berbeda jika Anda mentransmisikan ke, const unsignedbukan hanya unsigneddalam kasus yang tidak dioptimalkan.
Tandai Tebusan
2
@MarkRansom Saya rasa itu seharusnya tidak membuat perbedaan: "Janji" menjadi const hanya selama perbandingan tunggal, bukan untuk keseluruhan putaran
Hagen von Eitzen
13
Harap JANGAN PERNAH menggunakan fungsi ini mainuntuk menguji pengoptimalan. Gcc sengaja menandainya sebagai dingin dan dengan demikian menonaktifkan beberapa pengoptimalan untuknya. Saya tidak tahu apakah itu masalahnya di sini, tetapi itu adalah kebiasaan penting untuk dilakukan.
Marc Glisse
3
@MarcGlisse Anda 100% benar. Saya telah menulisnya dengan terburu-buru, saya akan memperbaikinya.
YSC
3
Berikut ini tautan ke kedua fungsi dalam satu unit kompilasi pada godbolt , dengan asumsi bitmapadalah global. Versi non-CSEd menggunakan operan memori ke cmp, yang tidak menjadi masalah kinerja dalam kasus ini. Jika itu lokal, kompilator bisa berasumsi pointer lain tidak bisa "tahu tentang" itu dan menunjuk ke dalamnya. Bukan ide yang buruk untuk menyimpan ekspresi yang melibatkan global dalam variabel temp, selama itu meningkatkan (atau tidak mengganggu) keterbacaan, atau jika performa sangat penting. Kecuali ada banyak hal yang terjadi, penduduk setempat seperti itu biasanya hanya tinggal di register, dan tidak akan pernah tumpah.
Peter Cordes
38

Sebenarnya ada informasi yang tidak cukup dari cuplikan kode Anda untuk dapat diceritakan, dan satu hal yang dapat saya pikirkan adalah aliasing. Dari sudut pandang kami, cukup jelas bahwa Anda tidak ingin pdan bitmapmenunjuk ke lokasi yang sama di memori, tetapi kompilator tidak tahu itu dan (karena ptipe char*) kompilator harus membuat kode ini berfungsi bahkan jika pdan bitmaptumpang tindih.

Ini berarti dalam kasus ini bahwa jika loop berubah bitmap->widthmelalui pointer pmaka itu harus dilihat saat membaca ulang bitmap->widthnanti, yang pada gilirannya berarti menyimpannya dalam variabel lokal akan ilegal.

Karena itu, saya yakin beberapa kompiler terkadang benar-benar menghasilkan dua versi dari kode yang sama (saya telah melihat bukti tidak langsung dari ini, tetapi tidak pernah secara langsung mencari informasi tentang apa yang dilakukan kompiler dalam kasus ini), dan dengan cepat memeriksa apakah petunjuknya alias dan jalankan kode yang lebih cepat jika dianggap tidak apa-apa.

Yang sedang berkata, saya mendukung komentar saya tentang hanya mengukur kinerja dua versi, uang saya tidak melihat perbedaan kinerja yang konsisten antara dua versi kode.

Menurut pendapat saya, pertanyaan seperti ini boleh-boleh saja jika tujuan Anda adalah mempelajari teori dan teknik pengoptimalan compiler, tetapi hanya membuang-buang waktu (pengoptimalan mikro yang tidak berguna) jika tujuan akhir Anda di sini adalah membuat program berjalan lebih cepat.

SirGuy
sumber
1
@GuyGreer: Ini adalah pemblokir pengoptimalan utama; Saya menganggap sangat disayangkan bahwa aturan bahasa berfokus pada aturan tentang tipe yang efektif, daripada mengidentifikasi situasi di mana penulisan dan pembacaan item yang berbeda diurutkan atau tidak. Aturan yang ditulis dalam istilah seperti itu dapat melakukan pekerjaan yang jauh lebih baik dalam memenuhi kebutuhan kompiler dan pemrogram daripada yang sekarang.
supercat
3
@GuyGreer - bukankah restrictkualifikasi menjadi jawaban untuk masalah aliasing dalam kasus ini?
LThode
4
Dalam pengalaman saya, restrictsebagian besar untung-untungan. MSVC adalah satu-satunya kompiler yang pernah saya lihat yang tampaknya melakukannya dengan benar. ICC kehilangan info aliasing melalui pemanggilan fungsi meskipun mereka sebaris. Dan GCC biasanya gagal mendapatkan manfaat apa pun kecuali Anda mendeklarasikan setiap parameter input sebagai restrict(termasuk thisuntuk fungsi anggota).
Mysticial
1
@Mystical: Satu hal yang perlu diingat adalah charalias semua jenis, jadi jika Anda memiliki char * maka Anda harus menggunakan restrictsemuanya. Atau jika Anda telah memaksa aturan aliasing ketat GCC off dengan -fno-strict-aliasingsemua itu dianggap alias yang mungkin.
Zan Lynx
1
@Ray Proposal terbaru untuk restrict-seperti semantik di C ++ adalah N4150 .
TC
24

Ok teman-teman, jadi saya sudah mengukur, dengan GCC -O3(menggunakan GCC 4.9 di Linux x64).

Ternyata, versi kedua bekerja 54% lebih cepat!

Jadi, saya kira aliasing adalah masalahnya, saya belum memikirkannya.

[Sunting]

Saya sudah mencoba lagi versi pertama dengan semua petunjuk yang ditentukan dengan __restrict__, dan hasilnya sama. Aneh .. Entah aliasing bukanlah masalah, atau, untuk beberapa alasan, kompilator tidak mengoptimalkannya dengan baik bahkan dengan __restrict__.

[Sunting 2]

Oke, saya rasa saya cukup bisa membuktikan bahwa aliasing adalah masalahnya. Saya mengulangi pengujian asli saya, kali ini menggunakan array daripada pointer:

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

Dan diukur (harus menggunakan "-mcmodel = large" untuk menghubungkannya). Kemudian saya mencoba:

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

Hasil pengukurannya sama - Sepertinya kompilator dapat mengoptimalkannya sendiri.

Kemudian saya mencoba kode asli (dengan penunjuk p), kali ini ketika pbertipe std::uint16_t*. Sekali lagi, hasilnya sama - karena aliasing yang ketat. Kemudian saya mencoba membangun dengan "-fno-strict-aliasing", dan sekali lagi melihat perbedaan waktu.

Yaron Cohen-Tal
sumber
4
Sepertinya ini harus menjadi komentar, meskipun secara teknis menjawab pertanyaan itu. Perhatikan juga, sayangnya Anda belum menunjukkan bahwa aliasing adalah masalahnya. Tampaknya mungkin, tentu masuk akal, tetapi itu berbeda dengan menyimpulkan bahwa itu saja.
SirGuy
@GuyGreer: Lihat [edit 2] saya - sekarang saya pikir itu cukup banyak terbukti.
Yaron Cohen-Tal
2
Saya hanya bertanya-tanya mengapa Anda mulai menggunakan variabel "i" ketika Anda memiliki "x" dalam lingkaran Anda?
Jesper Madsen
1
Apakah hanya saya yang merasa frasa 54% lebih cepat sulit untuk dipahami? Apakah maksud Anda itu 1,54 kali kecepatan yang tidak dioptimalkan, atau sesuatu yang lain?
Roddy
3
@ YaronCohen-Tal jadi dua kali lebih cepat? Mengesankan, tapi bukan yang saya pahami artinya "54% lebih cepat"!
Roddy
24

Jawaban lain telah menunjukkan bahwa mengangkat operasi pointer keluar dari loop dapat mengubah perilaku yang ditentukan karena aturan aliasing yang memungkinkan char menjadi alias apa pun dan karenanya bukan pengoptimalan yang diizinkan untuk kompiler meskipun dalam banyak kasus itu jelas benar untuk manusia. programmer.

Mereka juga telah menunjukkan bahwa mengangkat operasi keluar dari loop biasanya tetapi tidak selalu merupakan perbaikan dari sudut pandang kinerja dan seringkali negatif dari sudut pandang keterbacaan.

Saya ingin menunjukkan bahwa sering kali ada "cara ketiga". Daripada menghitung hingga jumlah iterasi yang Anda inginkan, Anda dapat menghitung mundur hingga nol. Artinya, jumlah iterasi hanya diperlukan satu kali di awal loop, tidak harus disimpan setelah itu. Lebih baik lagi di tingkat assembler, ia sering menghilangkan kebutuhan akan perbandingan eksplisit karena operasi pengurangan biasanya akan menetapkan tanda yang menunjukkan apakah penghitungnya nol baik sebelum (membawa bendera) dan setelah (bendera nol) penurunan.

for (unsigned x = static_cast<unsigned>(bitmap->width);x > 0;  x--)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

Perhatikan bahwa versi pengulangan ini memberikan nilai x dalam kisaran 1..lebar daripada kisaran 0 .. (lebar-1). Itu tidak masalah dalam kasus Anda karena Anda sebenarnya tidak menggunakan x untuk apa pun tetapi itu adalah sesuatu yang harus diperhatikan. Jika Anda menginginkan loop hitung mundur dengan nilai x dalam kisaran 0 .. (lebar-1) bisa Anda lakukan.

for (unsigned x = static_cast<unsigned>(bitmap->width); x-- > 0;)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

Anda juga dapat menyingkirkan cast pada contoh di atas jika Anda mau tanpa mengkhawatirkan pengaruhnya terhadap aturan perbandingan karena semua yang Anda lakukan dengan bitmap-> width adalah menugaskannya langsung ke variabel.

plugwash
sumber
2
Saya telah melihat kasus kedua diformat sebagai x --> 0, menghasilkan operator "downto". Cukup lucu. PS Saya tidak menganggap membuat variabel untuk kondisi akhir menjadi negatif untuk keterbacaan, sebenarnya bisa sebaliknya.
Tandai Tebusan
Itu benar-benar tergantung, kadang-kadang sebuah pernyataan menjadi sangat mengerikan sehingga memecahnya menjadi beberapa pernyataan meningkatkan keterbacaan tetapi saya tidak percaya itu yang terjadi di sini.
plugwash
1
+1 Pengamatan yang baik, meskipun saya berpendapat bahwa mengangkat static_cast<unsigned>(bitmap->width)dan menggunakan widthsebagai gantinya dalam loop sebenarnya merupakan peningkatan untuk keterbacaan karena sekarang ada lebih sedikit hal yang harus diurai oleh pembaca per baris. Pandangan orang lain mungkin berbeda.
SirGuy
1
Ada banyak situasi lain di mana menghitung mundur lebih baik (misalnya saat menghapus item dari daftar). Saya tidak tahu mengapa ini tidak dilakukan lebih sering.
Ian Goldby
3
Jika Anda ingin menulis loop yang terlihat lebih seperti asm optimal, gunakan do { } while(), karena di ASM Anda membuat loop dengan cabang bersyarat di bagian akhir. Biasa for(){}dan while(){}loop memerlukan instruksi tambahan untuk menguji kondisi loop satu kali sebelum loop, jika compiler tidak dapat membuktikannya selalu berjalan setidaknya satu kali. Dengan segala cara, gunakan for()atau while()kapan berguna untuk memeriksa apakah loop bahkan harus berjalan sekali, atau ketika lebih mudah dibaca.
Peter Cordes
11

Satu-satunya hal di sini yang dapat mencegah pengoptimalan adalah aturan aliasing yang ketat . Singkatnya :

"Strict aliasing adalah sebuah asumsi, yang dibuat oleh compiler C (atau C ++), bahwa pointer dereferensi ke objek dari jenis yang berbeda tidak akan pernah merujuk ke lokasi memori yang sama (yaitu alias satu sama lain.)"

[…]

Pengecualian untuk aturan tersebut adalah a char* , yang diizinkan untuk menunjuk ke tipe apa pun.

Pengecualian juga berlaku untuk unsigneddansigned char pointer .

Ini adalah kasus dalam kode Anda: Anda memodifikasi sedang *pmelalui pyang merupakan unsigned char*, sehingga compiler harus mengasumsikan bahwa itu bisa menunjukkan bitmap->width. Oleh karena itu, caching dari bitmap->widthadalah optimasi yang tidak valid. Perilaku pencegahan pengoptimalan ini ditunjukkan dalam jawaban YSC .

Jika dan hanya jika pdiarahkan ke non- chardan non- decltype(bitmap->width)tipe, apakah caching akan menjadi pengoptimalan yang memungkinkan.

emlai
sumber
10

Pertanyaan awalnya diajukan:

Apakah itu layak untuk dioptimalkan?

Dan jawaban saya untuk itu (mengumpulkan campuran yang bagus dari suara naik dan turun ..)

Biarkan kompiler mengkhawatirkannya.

Kompiler hampir pasti akan melakukan pekerjaan yang lebih baik dari Anda. Dan tidak ada jaminan bahwa 'pengoptimalan' Anda lebih baik daripada kode yang 'jelas' - sudahkah Anda mengukurnya ??

Lebih penting lagi, apakah Anda memiliki bukti bahwa kode yang Anda optimalkan berdampak pada kinerja program Anda?

Terlepas dari downvote (dan sekarang melihat masalah aliasing), saya masih senang dengan itu sebagai jawaban yang valid. Jika Anda tidak tahu apakah perlu mengoptimalkan sesuatu, mungkin tidak.

Pertanyaan yang agak berbeda, tentu saja, adalah ini:

Bagaimana cara mengetahui apakah perlu mengoptimalkan sebuah fragmen kode?

Pertama, apakah aplikasi atau pustaka Anda perlu berjalan lebih cepat daripada saat ini? Apakah pengguna menunggu terlalu lama? Apakah perangkat lunak Anda meramalkan cuaca kemarin, bukan besok?

Hanya Anda yang benar-benar dapat mengetahui hal ini, berdasarkan untuk apa perangkat lunak Anda dan apa yang diharapkan pengguna Anda.

Dengan asumsi perangkat lunak Anda memerlukan pengoptimalan, hal berikutnya yang harus dilakukan adalah mulai mengukur. Profiler akan memberi tahu Anda di mana kode Anda menghabiskan waktunya. Jika fragmen Anda tidak muncul sebagai hambatan, sebaiknya biarkan saja. Profiler dan alat ukur lainnya juga akan memberi tahu Anda jika perubahan Anda telah membuat perbedaan. Anda dapat menghabiskan waktu berjam-jam untuk mencoba mengoptimalkan kode, hanya untuk mengetahui bahwa Anda tidak membuat perbedaan yang terlihat.

Apa yang Anda maksud dengan 'mengoptimalkan'?

Jika Anda tidak menulis kode 'dioptimalkan', maka kode Anda harus sejelas, bersih, dan ringkas seperti yang Anda bisa membuatnya. Argumen "Pengoptimalan prematur itu jahat" bukanlah alasan untuk kode yang ceroboh atau tidak efisien.

Kode yang dioptimalkan biasanya mengorbankan beberapa atribut di atas untuk performa. Ini bisa melibatkan pengenalan variabel lokal tambahan, memiliki objek dengan cakupan yang lebih luas dari yang diharapkan atau bahkan membalik urutan loop normal. Semua ini mungkin kurang jelas atau ringkas, jadi dokumentasikan kodenya (secara singkat!) Tentang mengapa Anda melakukan ini.

Namun seringkali, dengan kode 'lambat', pengoptimalan mikro ini adalah pilihan terakhir. Tempat pertama untuk melihat adalah algoritme dan struktur data. Adakah cara untuk menghindari melakukan pekerjaan sama sekali? Bisakah pencarian linier diganti dengan pencarian biner? Apakah daftar tertaut lebih cepat di sini daripada vektor? Atau tabel hash? Bisakah saya menyimpan hasil? Membuat keputusan 'efisien' yang baik di sini sering kali dapat memengaruhi kinerja dengan urutan besarnya atau lebih!

Roddy
sumber
12
Saat Anda melakukan iterasi pada lebar gambar bitmap, logika perulangan bisa menjadi bagian signifikan dari waktu yang dihabiskan di loop. Daripada mengkhawatirkan pengoptimalan prematur, lebih baik dalam hal ini mengembangkan praktik terbaik yang efisien dari awal.
Markus Tebusan
4
@MarkRansom setuju, sebagian: Tapi "praktik terbaik" bisa berupa: menggunakan pustaka atau panggilan API yang ada untuk mengisi gambar, atau b: meminta GPU untuk melakukannya untuk Anda. Ini tidak boleh menjadi jenis pengoptimalan mikro tak terukur yang disarankan OP. Dan bagaimana Anda tahu kode ini pernah dieksekusi lebih dari sekali, atau dengan bitmap yang lebih besar dari lebar 16 piksel ...?
Roddy
@Tokopedia Hargai pembenaran untuk -1. Dorongan pertanyaan telah berubah secara halus dan substansial sejak saya menjawab. Jika menurut Anda jawaban (yang diperluas) masih tidak membantu, waktu bagi saya untuk menghapusnya ... "Apakah itu layak ..." pada dasarnya selalu didasarkan pada opini.
Roddy
@Roddy Saya menghargai hasil editnya, mereka benar-benar membantu (dan komentar saya mungkin terdengar terlalu kasar). Saya masih ragu, karena ini benar-benar jawaban untuk pertanyaan yang tidak sesuai untuk Stack Overflow. Sepertinya jawaban yang tepat akan spesifik untuk cuplikan tersebut, karena jawaban yang paling banyak dipilih di sini.
Veedrac
6

Saya menggunakan pola berikut dalam situasi seperti ini. Ini hampir sependek kasus pertama Anda, dan lebih baik daripada kasus kedua, karena itu membuat variabel sementara tetap lokal ke loop.

for (unsigned int x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
{
  *p++ = 0xAA;
  *p++ = 0xBB;
  *p++ = 0xCC;
}

Ini akan lebih cepat dengan compiler kurang dari smart, build debug, atau tanda kompilasi tertentu.

Sunting1 : Menempatkan operasi konstan di luar loop adalah pola pemrograman yang baik . Ini menunjukkan pemahaman tentang dasar-dasar operasi mesin, terutama di C / C ++. Saya berpendapat bahwa upaya untuk membuktikan diri harus dilakukan pada orang yang tidak mengikuti praktik ini. Jika kompilator menghukum untuk pola yang baik, itu adalah bug di kompilator.

Sunting2:: Saya telah mengukur saran saya terhadap kode asli pada vs2013, mendapat peningkatan% 1. Bisakah kita berbuat lebih baik? Pengoptimalan manual sederhana memberikan peningkatan 3 kali lipat dari loop asli pada mesin x64 tanpa menggunakan instruksi yang tidak biasa. Kode di bawah ini mengasumsikan sistem little endian dan bitmap selaras dengan benar. TEST 0 adalah asli (9 detik), TEST 1 lebih cepat (3 detik). Saya yakin seseorang dapat membuat ini lebih cepat, dan hasil pengujian akan bergantung pada ukuran bitmap. Pasti segera di masa depan, compiler akan dapat menghasilkan kode tercepat secara konsisten. Saya khawatir ini akan menjadi masa depan ketika kompiler juga akan menjadi programmer AI, jadi kami akan kehilangan pekerjaan. Tetapi untuk saat ini, cukup tulis kode yang menunjukkan bahwa Anda tahu bahwa operasi tambahan dalam loop tidak diperlukan.

#include <memory>
#include <time.h>

struct Bitmap_line
{
  int blah;
  unsigned int width;
  Bitmap_line(unsigned int w)
  {
    blah = 0;
    width = w;
  }
};

#define TEST 0 //define 1 for faster test

int main(int argc, char* argv[])
{
  unsigned int size = (4 * 1024 * 1024) / 3 * 3; //makes it divisible by 3
  unsigned char* pointer = (unsigned char*)malloc(size);
  memset(pointer, 0, size);
  std::unique_ptr<Bitmap_line> bitmap(new Bitmap_line(size / 3));
  clock_t told = clock();
#if TEST == 0
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
    //for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#else
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    unsigned x = 0;
    for (const unsigned n = static_cast<unsigned>(bitmap->width) - 4; x < n; x += 4)
    {
      *(int64_t*)p = 0xBBAACCBBAACCBBAALL;
      p += 8;
      *(int32_t*)p = 0xCCBBAACC;
      p += 4;
    }

    for (const unsigned n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#endif
  double ms = 1000.0 * double(clock() - told) / CLOCKS_PER_SEC;
  printf("time %0.3f\n", ms);

  {
    //verify
    unsigned char* p = pointer;
    for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      if ((*p++ != 0xAA) || (*p++ != 0xBB) || (*p++ != 0xCC))
      {
        printf("EEEEEEEEEEEEERRRRORRRR!!!\n");
        abort();
      }
    }
  }

  return 0;
}
0kcats
sumber
Anda dapat menghemat 25% lagi pada 64bit jika Anda menggunakan tiga int64_t daripada int64_t dan int32_t.
Antonín Lejsek
5

Ada dua hal yang perlu diperhatikan.

A) Seberapa sering pengoptimalan akan berjalan?

Jika jawabannya tidak terlalu sering, seperti hanya ketika pengguna mengklik tombol, maka jangan repot-repot jika membuat kode Anda tidak dapat dibaca. Jika jawabannya 1000 kali per detik maka Anda mungkin ingin melanjutkan dengan pengoptimalan. Jika ini agak rumit, pastikan untuk memberikan komentar untuk menjelaskan apa yang terjadi untuk membantu pria berikutnya yang datang.

B) Apakah ini akan membuat kode lebih sulit untuk dipelihara / dipecahkan?

Jika Anda tidak melihat peningkatan besar dalam kinerja, maka membuat kode Anda samar hanya untuk menghemat beberapa waktu bukanlah ide yang baik. Banyak orang akan memberi tahu Anda bahwa programmer yang baik harus dapat melihat kode dan mencari tahu apa yang sedang terjadi. Ini benar. Masalahnya adalah bahwa dalam dunia bisnis, waktu ekstra untuk menghitungnya membutuhkan uang. Jadi, jika Anda bisa membuatnya lebih cantik untuk dibaca, lakukanlah. Temanmu akan berterima kasih untuk itu.

Yang mengatakan saya pribadi akan menggunakan contoh B.

soulsabr
sumber
4

Kompiler dapat mengoptimalkan banyak hal. Sebagai contoh, Anda harus mencari readability, mantainability dan apa yang mengikuti standar kode Anda. Untuk informasi lebih lanjut tentang apa yang dapat dioptimalkan (dengan GCC), lihat posting blog ini .

Guillaume Racicot
sumber
4

Sebagai aturan umum, biarkan kompilator melakukan pengoptimalan untuk Anda, sampai Anda memutuskan bahwa Anda harus mengambil alih. Logika untuk ini tidak ada hubungannya dengan kinerja, melainkan dengan keterbacaan manusia. Dalam sebagian besar kasus, keterbacaan program Anda lebih penting daripada kinerjanya. Anda harus bertujuan untuk menulis kode yang lebih mudah dibaca oleh manusia, dan kemudian hanya mengkhawatirkan tentang pengoptimalan jika Anda yakin bahwa kinerja lebih penting daripada pemeliharaan kode Anda.

Setelah Anda melihat bahwa performa itu penting, Anda harus menjalankan profiler pada kode untuk menentukan loop mana yang tidak efisien, dan mengoptimalkannya satu per satu. Mungkin memang ada kasus di mana Anda ingin melakukan pengoptimalan itu (terutama jika Anda bermigrasi ke C ++, di mana penampung STL terlibat), tetapi biaya dalam hal keterbacaan sangat besar.

Selain itu, saya dapat memikirkan situasi patologis yang sebenarnya dapat memperlambat kode. Misalnya, pertimbangkan kasus di mana kompilator tidak dapat membuktikan bahwa bitmap->widthitu konstan selama proses. Dengan menambahkan widthvariabel, Anda memaksa kompilator untuk mempertahankan variabel lokal dalam cakupan itu. Jika, karena alasan khusus platform, variabel ekstra itu mencegah beberapa optimasi ruang-tumpukan, mungkin harus mengatur ulang bagaimana ia memancarkan bytecode, dan menghasilkan sesuatu yang kurang efisien.

Sebagai contoh, pada Windows x64, seseorang diwajibkan untuk memanggil API call khusus, __chkstkdalam pembukaan fungsi jika fungsi tersebut akan menggunakan lebih dari 1 halaman variabel lokal. Fungsi ini memberi jendela kesempatan untuk mengelola halaman penjaga yang mereka gunakan untuk memperluas tumpukan saat diperlukan. Jika variabel ekstra Anda mendorong penggunaan tumpukan dari bawah 1 halaman ke di-atau-di atas 1 halaman, fungsi Anda sekarang wajib dipanggil __chkstksetiap kali dimasukkan. Jika Anda mengoptimalkan loop ini pada jalur lambat, Anda sebenarnya dapat memperlambat jalur cepat lebih dari yang Anda simpan pada jalur lambat!

Tentu, ini sedikit patologis, tetapi inti dari contoh itu adalah Anda sebenarnya dapat memperlambat kompilator. Ini hanya menunjukkan bahwa Anda harus membuat profil pekerjaan Anda untuk menentukan ke mana pengoptimalan pergi. Sementara itu, harap jangan mengorbankan keterbacaan dengan cara apa pun untuk pengoptimalan yang mungkin atau mungkin tidak penting.

Cort Ammon
sumber
4
Saya berharap C dan C ++ akan memberikan lebih banyak cara untuk secara eksplisit mengidentifikasi hal-hal yang tidak dipedulikan oleh programmer. Tidak hanya akan memberikan lebih banyak kesempatan bagi kompiler untuk mengoptimalkan sesuatu, tetapi juga akan menyelamatkan programmer lain yang membaca kode dari keharusan menebak apakah misalnya mungkin memeriksa ulang bitmap-> lebar setiap kali untuk memastikan bahwa perubahan itu mempengaruhi loop, atau apakah itu mungkin caching bitmap-> width untuk memastikan bahwa perubahan itu tidak mempengaruhi loop. Memiliki sarana untuk mengatakan "Simpan ini atau tidak - saya tidak peduli" akan menjelaskan alasan pilihan programmer.
supercat
@supercat Saya setuju sepenuh hati, karena orang dapat melihat jika seseorang melihat tumpukan bahasa gagal yang di-tatted yang saya coba tulis untuk menyelesaikannya. Saya merasa sangat sulit untuk mendefinisikan "apa" yang tidak dipedulikan seseorang tanpa begitu banyak sintaksis yang tidak saleh sehingga tidak sepadan. Saya melanjutkan pencarian saya dengan sia-sia.
Cort Ammon
Tidak mungkin untuk mendefinisikannya dalam semua kasus, tetapi saya pikir ada banyak kasus di mana sistem tipe dapat membantu. Itu terlalu C memutuskan untuk membuat tipe karakter sebagai "pengakses universal" daripada memiliki kualifikasi tipe yang sedikit lebih longgar daripada "volatile" yang dapat diterapkan ke tipe apa pun , dengan semantik yang mengakses tipe seperti itu akan diproses secara berurutan dengan akses jenis setara yang tidak memenuhi syarat dan juga dengan akses semua jenis variabel dengan kualifikasi yang sama. Itu akan membantu memperjelas apakah seseorang menggunakan tipe karakter karena seseorang membutuhkan ...
supercat
... perilaku aliasing, atau apakah seseorang menggunakannya karena ukurannya tepat untuk memenuhi kebutuhannya. Ini juga akan membantu untuk memiliki penghalang aliasing explciit yang dalam banyak kasus dapat ditempatkan di luar loop, tidak seperti penghalang implisit yang terkait dengan akses tipe karakter.
supercat
1
Ini adalah pembicaraan yang bijak, tetapi, umumnya, jika Anda sudah memilih C untuk tugas Anda, mungkin kinerjanya sangat penting dan aturan yang berbeda harus diterapkan. Jika tidak, mungkin lebih baik menggunakan Ruby, Java, Python atau sejenisnya.
Audrius Meskauskas
4

The perbandingan yang salah sejak dua potongan kode

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)

dan

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x<width ;  ++x)

tidak setara

Dalam kasus pertama widthadalah dependen dan bukan const, dan orang tidak dapat berasumsi bahwa itu mungkin tidak berubah di antara iterasi berikutnya. Jadi tidak dapat dioptimalkan, tetapi harus diperiksa di setiap loop .

Dalam kasus Anda yang dioptimalkan, variabel lokal diberi nilai bitmap->widthdi beberapa titik selama eksekusi program. Kompilator dapat memverifikasi bahwa ini tidak benar-benar berubah.

Apakah Anda berpikir tentang multi threading, atau mungkin nilainya dapat bergantung secara eksternal sedemikian rupa sehingga nilainya tidak stabil. Bagaimana seseorang mengharapkan kompilator untuk mengetahui semua hal ini jika Anda tidak memberi tahu?

Kompilator hanya dapat melakukan sebaik yang dimungkinkan oleh kode Anda.

g24l
sumber
2

Kecuali Anda tahu bagaimana tepatnya kompilator mengoptimalkan kode, lebih baik lakukan pengoptimalan Anda sendiri dengan menjaga keterbacaan kode, dan desain. Praktisnya sulit untuk memeriksa kode assembly untuk setiap fungsi yang kami tulis untuk versi compiler baru.

Vinayak SM
sumber
1

Kompilator tidak dapat mengoptimalkan bitmap->widthkarena nilai dari widthdapat diubah di antara iterasi. Ada beberapa alasan paling umum:

  1. Multi-threading. Penyusun tidak dapat memprediksi apakah thread lain akan mengubah nilai.
  2. Modifikasi di dalam loop, terkadang tidak mudah untuk mengetahui apakah variabel akan diubah di dalam loop.
  3. Hal ini fungsi panggilan, misalnya iterator::end()atau container::size()sehingga sulit untuk memprediksi apakah itu akan selalu mengembalikan hasil yang sama.

Untuk meringkas (pendapat pribadi saya) untuk tempat-tempat yang membutuhkan optimasi tingkat tinggi Anda perlu melakukannya sendiri, di tempat lain biarkan saja, kompiler dapat mengoptimalkannya atau tidak, jika tidak ada perbedaan besar pembacaan kode adalah target utama.

ST3
sumber