Saya baru-baru ini mewawancarai di Amazon. Selama sesi pengkodean, pewawancara bertanya mengapa saya mendeklarasikan variabel dalam suatu metode. Saya menjelaskan proses saya dan dia menantang saya untuk memecahkan masalah yang sama dengan lebih sedikit variabel. Sebagai contoh (ini bukan dari wawancara), saya mulai dengan Metode A kemudian meningkatkannya ke Metode B, dengan menghapus int s
. Dia senang dan mengatakan ini akan mengurangi penggunaan memori dengan metode ini.
Saya mengerti logika di baliknya, tetapi pertanyaan saya adalah:
Kapan tepat menggunakan Metode A vs Metode B, dan sebaliknya?
Anda dapat melihat bahwa Metode A akan memiliki penggunaan memori yang lebih tinggi, karena int s
dideklarasikan, tetapi hanya perlu melakukan satu perhitungan, yaitu a + b
. Di sisi lain, Metode B memiliki penggunaan memori yang lebih rendah, tetapi harus melakukan dua perhitungan, yaitu a + b
dua kali. Kapan saya menggunakan satu teknik di atas yang lain? Atau, apakah salah satu teknik selalu lebih disukai daripada yang lain? Apa hal yang perlu dipertimbangkan ketika mengevaluasi dua metode?
Metode A:
private bool IsSumInRange(int a, int b)
{
int s = a + b;
if (s > 1000 || s < -1000) return false;
else return true;
}
Metode B:
private bool IsSumInRange(int a, int b)
{
if (a + b > 1000 || a + b < -1000) return false;
else return true;
}
sumber
int s
sementara benar-benar baik-baik saja dengan angka ajaib itu untuk batas atas dan bawah?Jawaban:
Daripada berspekulasi tentang apa yang mungkin atau tidak mungkin terjadi, mari kita lihat, ya? Saya harus menggunakan C ++ karena saya tidak memiliki kompiler C # berguna (meskipun lihat contoh C # dari VisualMelon ), tapi saya yakin prinsip yang sama berlaku terlepas.
Kami akan menyertakan dua alternatif yang Anda temui dalam wawancara. Kami juga akan menyertakan versi yang digunakan
abs
seperti yang disarankan oleh beberapa jawaban.Sekarang kompilasi tanpa optimasi apa pun:
g++ -c -o test.o test.cpp
Sekarang kita dapat melihat dengan tepat apa yang dihasilkannya:
objdump -d test.o
Kita dapat melihat dari alamat stack (misalnya,
-0x4
inmov %edi,-0x4(%rbp)
versus the-0x14
inmov %edi,-0x14(%rbp)
) yangIsSumInRangeWithVar()
menggunakan 16 byte tambahan pada stack.Karena
IsSumInRangeWithoutVar()
tidak mengalokasikan ruang pada tumpukan untuk menyimpan nilai menengah,s
ia harus menghitung ulang, sehingga implementasi ini menjadi 2 instruksi lebih lama.Lucu,
IsSumInRangeSuperOptimized()
terlihat sangat miripIsSumInRangeWithoutVar()
, kecuali membandingkan dengan -1000 pertama, dan 1000 detik.Sekarang mari kita mengkompilasi dengan hanya optimasi yang paling dasar:
g++ -O1 -c -o test.o test.cpp
. Hasil:Apakah Anda akan melihatnya: setiap varian identik . Kompiler dapat melakukan sesuatu yang cukup pintar:
abs(a + b) <= 1000
sama dengana + b + 1000 <= 2000
mempertimbangkansetbe
melakukan perbandingan yang tidak ditandatangani, sehingga angka negatif menjadi angka positif yang sangat besar. Thelea
instruksi benar-benar dapat melakukan semua penambahan ini dalam satu instruksi, dan menghilangkan semua cabang bersyarat.Untuk menjawab pertanyaan Anda, hampir selalu hal untuk dioptimalkan bukan memori atau kecepatan, tetapi keterbacaan . Membaca kode jauh lebih sulit daripada menulisnya, dan membaca kode yang telah rusak untuk "mengoptimalkan" itu jauh lebih sulit daripada membaca kode yang telah ditulis menjadi jelas. Lebih sering daripada tidak, "optimasi" ini dapat diabaikan, atau seperti dalam kasus ini persis nol dampak aktual pada kinerja.
Ayo ukur! Saya telah menyalin contoh ke Python:
Jalankan dengan Python 3.5.2, ini menghasilkan output:
Disassembly dengan Python tidak terlalu menarik, karena bytecode "compiler" tidak banyak membantu dalam optimasi.
Kinerja ketiga fungsi ini hampir identik. Kita mungkin tergoda untuk pergi
IsSumInRangeWithVar()
karena kenaikan kecepatan marjinal. Meskipun saya akan menambahkan ketika saya mencoba parameter yang berbedatimeit
, kadangIsSumInRangeSuperOptimized()
- kadang keluar tercepat, jadi saya curiga itu mungkin faktor eksternal yang bertanggung jawab atas perbedaan, daripada keuntungan intrinsik dari implementasi apa pun.Jika ini benar-benar kode kritis kinerja, bahasa yang ditafsirkan hanyalah pilihan yang sangat buruk. Menjalankan program yang sama dengan pypy, saya dapat:
Hanya menggunakan pypy, yang menggunakan kompilasi JIT untuk menghilangkan banyak overhead juru, telah menghasilkan peningkatan kinerja sebesar 1 atau 2 kali lipat. Saya cukup terkejut melihat
IsSumInRangeWithVar()
urutan besarnya lebih cepat dari yang lain. Jadi saya mengubah urutan tolok ukur dan berlari lagi:Jadi sepertinya sebenarnya bukan apa-apa tentang implementasi yang membuatnya cepat, melainkan urutan di mana saya melakukan benchmarking!
Saya ingin menggali ini lebih dalam, karena jujur saya tidak tahu mengapa ini terjadi. Tapi saya percaya intinya telah dibuat: optimasi mikro seperti apakah menyatakan nilai menengah sebagai variabel atau tidak jarang relevan. Dengan bahasa yang ditafsirkan atau kompiler yang sangat optimal, tujuan pertama adalah tetap menulis kode yang jelas.
Jika optimasi lebih lanjut mungkin diperlukan, patokan . Ingat bahwa optimisasi terbaik tidak datang dari detail kecil tetapi gambaran algoritmik yang lebih besar: pypy akan menjadi urutan besarnya lebih cepat untuk evaluasi berulang dari fungsi yang sama dari cpython karena menggunakan algoritma yang lebih cepat (JIT compiler vs interpretasi) untuk mengevaluasi program. Dan ada algoritma berkode untuk dipertimbangkan juga: pencarian melalui B-tree akan lebih cepat daripada daftar yang ditautkan.
Setelah memastikan Anda menggunakan alat dan algoritme yang tepat untuk pekerjaan itu, bersiaplah untuk menyelami lebih dalam rincian sistem. Hasilnya bisa sangat mengejutkan, bahkan untuk pengembang berpengalaman, dan inilah sebabnya Anda harus memiliki tolok ukur untuk menghitung perubahan.
sumber
Untuk menjawab pertanyaan yang disebutkan:
Ada dua hal yang harus Anda bangun:
Untuk menjawab pertanyaan pertama, Anda harus tahu apa persyaratan kinerja untuk aplikasi Anda. Jika tidak ada persyaratan kinerja maka tidak ada alasan untuk mengoptimalkan satu atau lain cara. Persyaratan kinerja membantu Anda mencapai "cukup baik".
Metode yang Anda berikan sendiri tidak akan menyebabkan masalah kinerja sendiri, tetapi mungkin dalam satu lingkaran dan memproses sejumlah besar data, Anda harus mulai berpikir sedikit berbeda tentang bagaimana Anda mendekati masalah.
Mendeteksi apa yang membatasi aplikasi
Mulailah melihat perilaku aplikasi Anda dengan monitor kinerja. Mengawasi penggunaan CPU, disk, jaringan, dan memori saat sedang berjalan. Satu atau lebih item akan dimaksimalkan sementara yang lainnya cukup digunakan - kecuali Anda mencapai keseimbangan sempurna, tetapi itu hampir tidak pernah terjadi).
Ketika Anda perlu melihat lebih dalam, biasanya Anda akan menggunakan profiler . Ada profiler memori dan profiler proses , dan mereka mengukur hal-hal yang berbeda. Tindakan profiling memang memiliki dampak kinerja yang signifikan, tetapi Anda menginstruksikan kode Anda untuk mencari tahu apa yang salah.
Katakanlah Anda melihat penggunaan CPU dan disk Anda memuncak. Pertama-tama Anda akan memeriksa "hot spot" atau kode yang disebut lebih sering daripada yang lain atau mengambil persentase pemrosesan yang jauh lebih lama.
Jika Anda tidak dapat menemukan hot spot, Anda akan mulai melihat memori. Mungkin Anda membuat lebih banyak objek dari yang diperlukan dan pengumpulan sampah Anda bekerja lembur.
Reklamasi kinerja
Berpikir kritis. Daftar perubahan berikut adalah dalam urutan berapa banyak pengembalian investasi yang akan Anda dapatkan:
Dalam situasi seperti ini, Anda harus menerapkan metode ilmiah. Munculkan hipotesis, buat perubahan, dan ujilah. Jika Anda memenuhi sasaran kinerja Anda, berarti Anda sudah selesai. Jika tidak, buka hal berikutnya dalam daftar.
Menjawab pertanyaan dengan berani:
Jujur, ini adalah langkah terakhir dalam mencoba menangani masalah kinerja atau memori. Dampak Metode A vs Metode B akan sangat berbeda tergantung pada bahasa dan platform (dalam beberapa kasus).
Hampir semua bahasa yang dikompilasi dengan pengoptimal yang layak setengah akan menghasilkan kode yang sama dengan salah satu dari struktur tersebut. Namun, asumsi tersebut tidak selalu benar dalam bahasa mainan dan kepemilikan yang tidak memiliki pengoptimal.
Justru yang akan memiliki dampak yang lebih baik tergantung pada apakah
sum
variabel tumpukan atau variabel tumpukan. Ini adalah pilihan implementasi bahasa. Dalam C, C ++ dan Java misalnya, bilangan primitif seperti aint
adalah variabel tumpukan secara default. Kode Anda tidak memiliki dampak kehabisan memori dengan menetapkan variabel stack daripada yang Anda miliki dengan kode sepenuhnya inline.Optimalisasi lain yang mungkin Anda temukan di pustaka C (terutama yang lebih tua) di mana Anda harus memutuskan antara menyalin array 2 dimensi ke bawah terlebih dahulu atau melintasi yang pertama adalah optimasi bergantung platform. Ini membutuhkan beberapa pengetahuan tentang bagaimana chipset yang Anda targetkan mengoptimalkan akses memori terbaik. Ada perbedaan halus antara arsitektur.
Intinya adalah optimasi adalah kombinasi seni dan sains. Dibutuhkan pemikiran kritis, serta tingkat fleksibilitas dalam cara Anda mendekati masalah. Cari hal-hal besar sebelum Anda menyalahkan hal-hal kecil.
sumber
"Ini akan mengurangi memori" - em, no. Bahkan jika ini benar (yang, untuk kompiler yang layak tidak), perbedaannya kemungkinan besar akan diabaikan untuk situasi dunia nyata.
Namun, saya akan merekomendasikan untuk menggunakan metode A * (metode A dengan sedikit perubahan):
tetapi karena dua alasan yang sangat berbeda:
dengan memberikan variabel
s
nama yang menjelaskan, kode menjadi lebih jelasitu menghindari untuk memiliki logika penjumlahan yang sama dua kali dalam kode, sehingga kode menjadi lebih KERING, yang berarti lebih sedikit kesalahan rentan terhadap perubahan.
sumber
sum
variabel, sehingga mengarah ke penggunaan nol memori. Dan bahkan jika tidak, ini hanya satu kata memori dalam metode "daun". Mempertimbangkan bagaimana Java atau C # yang sangat boros memori dapat terjadi karena GC dan model objeknya,int
variabel lokal secara harfiah tidak menggunakan memori yang terlihat. Ini adalah optimasi mikro yang tidak ada gunanya.sum
akan menjadi detail implementasi dan saya ragu ada yang bisa meyakinkan apakah ada trik konyol seperti menghindari satu lokalint
akan mengarah ke ini atau jumlah penggunaan memori dalam jangka panjang. Keterbacaan IMO lebih penting. Keterbacaan dapat bersifat subyektif, tetapi FWIW, secara pribadi saya lebih suka Anda tidak pernah melakukan perhitungan yang sama dua kali, bukan untuk penggunaan CPU, tetapi karena saya hanya perlu memeriksa tambahan Anda sekali ketika saya mencari bug.Anda dapat melakukan lebih baik dari keduanya
Kebanyakan prosesor (dan karenanya kompiler) dapat melakukan abs () dalam satu operasi. Anda tidak hanya memiliki jumlah yang lebih sedikit, tetapi juga lebih sedikit perbandingan, yang umumnya lebih mahal secara komputasi. Ini juga menghilangkan percabangan, yang jauh lebih buruk pada sebagian besar prosesor karena menghentikan pemipaan menjadi mungkin.
Pewawancara, seperti jawaban lain katakan, adalah kehidupan tanaman dan tidak memiliki bisnis melakukan wawancara teknis.
Yang mengatakan, pertanyaannya valid. Dan jawaban kapan Anda mengoptimalkan dan bagaimana, adalah ketika Anda telah membuktikannya perlu, dan Anda telah membuat profil untuk membuktikan dengan tepat bagian mana yang membutuhkannya . Knuth terkenal mengatakan bahwa optimasi prematur adalah akar dari semua kejahatan, karena terlalu mudah untuk mencoba membuat bagian yang tidak penting, atau membuat perubahan (seperti pewawancara Anda) yang tidak berpengaruh, sementara kehilangan tempat-tempat yang benar-benar membutuhkannya. Sampai Anda punya bukti keras itu benar-benar diperlukan, kejelasan kode adalah target yang lebih penting.
Sunting FabioTurati dengan benar menunjukkan bahwa ini adalah logika yang berlawanan dengan yang asli, (kesalahan saya!), Dan ini mengilustrasikan dampak lebih lanjut dari kutipan Knuth di mana kita berisiko melanggar kode ketika kita mencoba untuk mengoptimalkannya.
sumber
a+b
keif
dan melakukannya dua kali. Anda salah paham, "Dia senang dan mengatakan ini akan mengurangi penggunaan memori dengan metode ini" - dia baik kepada Anda, menyembunyikan kekecewaannya dengan penjelasan tidak bermakna tentang memori ini. Anda seharusnya tidak serius untuk mengajukan pertanyaan di sini. Apakah Anda mendapat pekerjaan? Dugaan Anda, Anda tidak :-(abs()
, dan Anda juga memiliki satureturn
, alih-alih memiliki satu ketika kondisinya benar ("jika bercabang") dan yang lain ketika itu salah ( "Cabang lain"). Ketika Anda mengubah kode seperti ini, berhati-hatilah: ada risiko untuk secara tidak sengaja menulis fungsi yang mengembalikan true ketika itu harus mengembalikan false, dan sebaliknya. Itulah tepatnya yang terjadi di sini. Saya tahu Anda berfokus pada hal lain, dan Anda telah melakukan pekerjaan dengan baik. Namun, ini bisa dengan mudah membuat Andaadds
, dan ARM telah memprediksikan reverse-sub (rsblt
= reverse-sub jika less-tha) tetapi yang lainnya membutuhkan beberapa instruksi tambahan untuk mengimplementasikanabs(a+b)
atauabs(a)
. godbolt.org/z/Ok_Con menunjukkan output x86, ARM, AArch64, PowerPC, MIPS, dan RISC-V. Hanya dengan mengubah perbandingan menjadi rentang-periksa(unsigned)(a+b+999) <= 1998U
bahwa gcc dapat mengoptimalkannya seperti dalam jawaban Phil.IsSumInRange(INT_MIN, 0)
. Kode asli kembalifalse
karenaINT_MIN+0 > 1000 || INT_MIN+0 < -1000
; tetapi kode "baru dan lebih baik" kembalitrue
karenaabs(INT_MIN+0) < 1000
. (Atau, dalam beberapa bahasa, itu akan menimbulkan pengecualian atau memiliki perilaku yang tidak terdefinisi. Periksa daftar lokal Anda.)Perangkat keras itu murah; programmer mahal . Jadi, biaya waktu yang Anda berdua habiskan untuk pertanyaan ini mungkin jauh lebih buruk daripada jawaban mana pun.
Apapun, kebanyakan kompiler modern akan menemukan cara untuk mengoptimalkan variabel lokal ke dalam register (daripada mengalokasikan ruang stack), sehingga metode mungkin identik dalam hal kode yang dapat dieksekusi. Untuk alasan ini, sebagian besar pengembang akan memilih opsi yang mengomunikasikan niat dengan paling jelas (lihat Menulis kode yang sangat jelas (ROC) ). Menurut pendapat saya, itu akan menjadi Metode A.
Di sisi lain, jika ini murni latihan akademis, Anda bisa mendapatkan yang terbaik dari kedua dunia dengan Metode C:
sumber
a+=b
adalah trik yang rapi tapi saya harus menyebutkan (kalau-kalau itu tidak tersirat dari sisa jawaban), dari metode pengalaman saya yang mengacaukan parameter bisa sangat sulit untuk debug dan pemeliharaan.if
cek, tetapi lupa membalikkan hasil perbandingan; fungsi Anda sekarang kembalitrue
ketikaa + b
ini tidak dalam kisaran. Tambahkan a!
ke bagian luar kondisi (return !(a > 1000 || a < -1000)
), atau bagikan!
tes, pembalik, untuk mendapatkanreturn a <= 1000 && a >= -1000;
atau untuk membuat aliran rentang periksa dengan baik,return -1000 <= a && a <= 1000;
<=
/>=
, bukan<
/>
(dengan<
/>
, 1000 dan -1000 diperlakukan sebagai di luar jangkauan, kode asli memperlakukannya seperti dalam jangkauan).Saya akan mengoptimalkan untuk keterbacaan. Metode X:
Metode kecil yang hanya melakukan 1 hal tetapi mudah dipikirkan.
(Ini adalah preferensi pribadi, saya lebih suka pengujian positif daripada negatif, kode asli Anda sebenarnya menguji apakah nilainya TIDAK di luar kisaran.)
sumber
a
danb
untuknumber1
dannumber2
membantu keterbacaan dengan cara apa pun. Penamaan fungsi Anda juga tidak konsisten: mengapaIsSumInRange
hard-code rentang jikaIsValueInRange
menerimanya sebagai argumen?Singkatnya, saya tidak berpikir pertanyaan itu memiliki banyak relevansi dalam komputasi saat ini, tetapi dari perspektif sejarah, ini adalah latihan pemikiran yang menarik.
Pewawancara Anda kemungkinan adalah penggemar Bulan Mythical Man. Dalam buku ini, Fred Brooks menyatakan bahwa para programmer umumnya membutuhkan dua versi fungsi-fungsi utama dalam kotak peralatan mereka: versi yang dioptimalkan-memori dan versi yang dioptimalkan-cpu. Fred mendasarkan ini pada pengalamannya memimpin pengembangan sistem operasi IBM System / 360 di mana mesin mungkin memiliki hanya 8 kilobyte RAM. Dalam mesin seperti itu, memori yang diperlukan untuk variabel lokal dalam fungsi berpotensi penting, terutama jika kompiler tidak secara efektif mengoptimalkannya (atau jika kode ditulis dalam bahasa assembly secara langsung).
Di era saat ini, saya pikir Anda akan sulit sekali menemukan sistem di mana ada atau tidak adanya variabel lokal dalam suatu metode akan membuat perbedaan yang nyata. Agar suatu variabel menjadi masalah, metode tersebut harus bersifat rekursif dengan rekursi dalam yang diharapkan. Bahkan kemudian, kemungkinan kedalaman tumpukan akan terlampaui yang menyebabkan pengecualian Stack Overflow sebelum variabel itu sendiri menyebabkan masalah. Satu-satunya skenario nyata di mana ia mungkin menjadi masalah adalah dengan array yang sangat besar, yang dialokasikan pada stack dalam metode rekursif. Tapi itu juga tidak mungkin karena saya pikir sebagian besar pengembang akan berpikir dua kali tentang salinan array besar yang tidak perlu.
sumber
Setelah penugasan s = a + b; variabel a dan b tidak digunakan lagi. Oleh karena itu, tidak ada memori yang digunakan untuk s jika Anda tidak menggunakan kompiler yang benar-benar rusak otak; memori yang digunakan untuk a dan b digunakan kembali.
Tetapi mengoptimalkan fungsi ini sama sekali tidak masuk akal. Jika Anda bisa menghemat ruang, mungkin 8 byte saat fungsi sedang berjalan (yang dipulihkan saat fungsi kembali), jadi sama sekali tidak ada gunanya. Jika Anda bisa menghemat waktu, itu akan menjadi satu nanodetik. Mengoptimalkan ini adalah total buang waktu.
sumber
Variabel tipe nilai lokal dialokasikan pada stack atau (lebih mungkin untuk potongan kode kecil seperti itu) menggunakan register di prosesor dan tidak pernah melihat RAM. Bagaimanapun mereka berumur pendek dan tidak ada yang perlu dikhawatirkan. Anda mulai mempertimbangkan penggunaan memori saat Anda perlu melakukan buffer atau mengantri elemen data dalam koleksi yang berpotensi besar dan berumur panjang.
Maka itu tergantung apa yang paling Anda pedulikan untuk aplikasi Anda. Kecepatan pemrosesan? Waktu merespon? Jejak memori? Kemampuan perawatan? Konsistensi dalam desain? Semua terserah Anda.
sumber
stackalloc
dan sekarangSpan<T>
. Mungkin bermanfaat di hot spot, setelah profil. Juga, beberapa dokumen di sekitar struct menyiratkan bahwa tipe nilai mungkin ada di stack sementara tipe referensi tidak akan. Bagaimanapun, yang terbaik Anda mungkin menghindari sedikit GC.Seperti jawaban lain katakan, Anda perlu memikirkan apa yang Anda optimalkan.
Dalam contoh ini, saya menduga bahwa setiap kompiler yang layak akan menghasilkan kode yang setara untuk kedua metode, sehingga keputusan tidak akan berpengaruh pada waktu berjalan atau memori!
Apa itu tidak mempengaruhi adalah pembacaan kode. (Kode diperuntukkan bagi manusia untuk dibaca, bukan hanya komputer.) Tidak ada terlalu banyak perbedaan antara dua contoh; ketika semua hal lain sama, saya menganggap keringkasan sebagai suatu kebajikan, jadi saya mungkin akan memilih Metode B. Tetapi semua hal lainnya jarang sama, dan dalam kasus dunia nyata yang lebih kompleks, itu bisa memiliki efek besar.
Hal yang perlu dipertimbangkan:
sumber
Seperti yang telah ditunjukkan oleh banyak jawaban, mencoba menyempurnakan fungsi ini dengan kompiler modern tidak akan membuat perbedaan. Pengoptimal kemungkinan besar dapat mencari solusi terbaik (pilih suara untuk jawaban yang menunjukkan kode assembler untuk membuktikannya!). Anda menyatakan bahwa kode dalam wawancara itu bukan kode yang harus Anda bandingkan, jadi mungkin contoh yang sebenarnya lebih masuk akal.
Tapi mari kita lihat lagi pertanyaan ini: ini adalah pertanyaan wawancara. Jadi masalah sebenarnya adalah, bagaimana Anda menjawabnya dengan asumsi Anda ingin mencoba dan mendapatkan pekerjaan?
Mari kita juga berasumsi bahwa pewawancara tahu apa yang mereka bicarakan dan mereka hanya mencoba melihat apa yang Anda ketahui.
Saya akan menyebutkan bahwa, mengabaikan pengoptimal, yang pertama dapat membuat variabel sementara di stack sedangkan yang kedua tidak, tetapi akan melakukan perhitungan dua kali. Oleh karena itu, yang pertama menggunakan lebih banyak memori tetapi lebih cepat.
Anda bisa menyebutkan itu, suatu perhitungan mungkin memerlukan variabel sementara untuk menyimpan hasilnya (agar dapat dibandingkan), jadi apakah Anda menyebutkan variabel itu atau tidak, mungkin tidak ada bedanya.
Saya kemudian akan menyebutkan bahwa dalam kenyataannya kode akan dioptimalkan dan kemungkinan besar kode mesin yang setara akan dihasilkan karena semua variabel lokal. Namun, itu tergantung pada apa yang Anda gunakan kompiler (itu belum lama bahwa saya bisa mendapatkan peningkatan kinerja yang berguna dengan mendeklarasikan variabel lokal sebagai "final" di Jawa).
Anda bisa menyebutkan bahwa tumpukan dalam kasus apa pun tinggal di halaman memori sendiri, jadi kecuali variabel tambahan Anda menyebabkan tumpukan meluap halaman, itu sebenarnya tidak akan mengalokasikan lebih banyak memori. Jika tidak meluap, ia akan menginginkan seluruh halaman baru.
Saya akan menyebutkan bahwa contoh yang lebih realistis mungkin adalah pilihan apakah akan menggunakan cache untuk menyimpan hasil banyak perhitungan atau tidak dan ini akan menimbulkan pertanyaan tentang cpu vs memori.
Semua ini menunjukkan bahwa Anda tahu apa yang Anda bicarakan.
Saya akan membiarkannya sampai akhir untuk mengatakan bahwa akan lebih baik untuk fokus pada keterbacaan sebagai gantinya. Meskipun benar dalam kasus ini, dalam konteks wawancara mungkin ditafsirkan sebagai "Saya tidak tahu tentang kinerja tetapi kode saya dibaca seperti cerita Janet dan John ".
Apa yang seharusnya tidak Anda lakukan adalah menghapus pernyataan hambar yang biasa tentang bagaimana optimasi kode tidak perlu, jangan optimalkan sampai Anda telah membuat profil kode (ini hanya menunjukkan Anda tidak dapat melihat kode yang buruk untuk diri Anda sendiri), biaya perangkat keras lebih murah daripada programmer , dan tolong, tolong, jangan mengutip Knuth "prematur bla bla ...".
Kinerja kode adalah masalah asli di banyak organisasi dan banyak organisasi membutuhkan programmer yang memahaminya.
Khususnya, dengan organisasi seperti Amazon, beberapa kode memiliki pengaruh besar. Cuplikan kode dapat digunakan pada ribuan server atau jutaan perangkat dan dapat disebut miliaran kali sehari setiap hari dalam setahun. Mungkin ada ribuan cuplikan serupa. Perbedaan antara algoritma yang buruk dan yang baik dapat dengan mudah menjadi faktor dari seribu. Lakukan angka dan gandakan semuanya: itu membuat perbedaan. Biaya potensial untuk organisasi kode yang tidak berkinerja bisa sangat signifikan atau bahkan fatal jika sistem kehabisan kapasitas.
Terlebih lagi, banyak dari organisasi ini bekerja di lingkungan yang kompetitif. Jadi Anda tidak bisa hanya memberi tahu pelanggan Anda untuk membeli komputer yang lebih besar jika perangkat lunak pesaing Anda sudah berfungsi dengan baik pada perangkat keras yang mereka miliki atau jika perangkat lunak tersebut berjalan pada handset seluler dan tidak dapat ditingkatkan. Beberapa aplikasi sangat kritis terhadap kinerja (permainan dan aplikasi seluler muncul di benak) dan dapat hidup atau mati sesuai dengan kecepatan atau responsnya.
Saya secara pribadi telah bekerja selama lebih dari dua dekade di banyak proyek di mana sistem gagal atau tidak dapat digunakan karena masalah kinerja dan saya dipanggil untuk mengoptimalkan sistem tersebut dan dalam semua kasus itu disebabkan oleh kode buruk yang ditulis oleh programmer yang tidak mengerti dampak dari apa yang mereka tulis. Terlebih lagi, ini tidak pernah menjadi bagian dari kode, selalu ada di mana-mana. Ketika saya muncul, itu adalah cara terlambat untuk mulai berpikir tentang kinerja: kerusakan telah terjadi.
Memahami kinerja kode adalah keterampilan yang baik untuk memiliki cara yang sama seperti memahami kebenaran kode dan gaya kode. Itu keluar dari latihan. Kegagalan kinerja bisa sama buruknya dengan kegagalan fungsional. Jika sistem tidak bekerja, itu tidak berfungsi. Tidak masalah mengapa. Demikian pula, kinerja dan fitur yang tidak pernah digunakan keduanya buruk.
Jadi, jika pewawancara bertanya tentang kinerja, saya akan merekomendasikan untuk mencoba dan menunjukkan sebanyak mungkin pengetahuan. Jika pertanyaannya tampak buruk, tunjukkan dengan sopan mengapa Anda pikir itu tidak akan menjadi masalah dalam kasus itu. Jangan mengutip Knuth.
sumber
Anda harus mengoptimalkan dulu untuk kebenaran.
Fungsi Anda gagal untuk nilai input yang dekat dengan Int.MaxValue:
Ini mengembalikan true karena jumlah meluap ke -400. Fungsi ini juga tidak berfungsi untuk = int.MinValue + 200. (salah menambahkan hingga "400")
Kita tidak akan tahu apa yang dicari pewawancara kecuali dia berpadu, tetapi "meluap itu nyata" .
Dalam situasi wawancara, ajukan pertanyaan untuk memperjelas ruang lingkup masalah: Apakah nilai input maksimum dan minimum yang diizinkan? Setelah memilikinya, Anda dapat melempar pengecualian jika pemanggil mengirimkan nilai di luar rentang. Atau (dalam C #), Anda dapat menggunakan bagian {} yang dicentang, yang akan memberikan pengecualian pada overflow. Ya, ini lebih sulit dan rumit, tetapi kadang-kadang itulah yang dibutuhkan.
sumber
Pertanyaan Anda seharusnya: "Apakah saya perlu mengoptimalkan ini sama sekali?".
Versi A dan B berbeda dalam satu detail penting yang membuat A preferrable, tetapi tidak terkait dengan optimasi: Anda tidak mengulangi kode.
"Optimalisasi" yang sebenarnya disebut eliminasi subekspresi umum, yang dilakukan oleh hampir semua kompiler. Beberapa melakukan optimasi dasar ini bahkan ketika optimasi dimatikan. Jadi itu tidak benar-benar optimasi (kode yang dihasilkan hampir pasti persis sama dalam setiap kasus).
Tetapi jika itu bukan optimasi, lalu mengapa itu lebih disukai? Baiklah, Anda tidak mengulangi kode, siapa yang peduli!
Yah pertama-tama, Anda tidak memiliki risiko tidak sengaja mendapatkan setengah dari klausa bersyarat yang salah. Tetapi yang lebih penting, seseorang yang membaca kode ini dapat langsung mengetahui apa yang Anda coba lakukan, alih-alih
if((((wtf||is||this||longexpression))))
pengalaman. Yang bisa dilihat pembaca adalahif(one || theother)
, hal yang bagus. Tidak jarang, saya kebetulan Anda adalah orang lain yang membaca kode Anda sendiri tiga tahun kemudian dan berpikir, "Apa artinya ini?" Dalam hal ini selalu membantu jika kode Anda segera mengomunikasikan maksudnya. Dengan subekspresi umum diberi nama dengan benar, itulah masalahnya.Juga, jika sewaktu-waktu di masa depan, Anda memutuskan bahwa misalnya Anda perlu mengubah
a+b
kea-b
, Anda harus mengubah satulokasi, bukan dua. Dan tidak ada risiko (lagi) mendapatkan yang kedua salah secara tidak sengaja.Tentang pertanyaan Anda yang sebenarnya, untuk apa Anda harus mengoptimalkan, pertama-tama kode Anda harus benar . Ini adalah hal yang paling penting. Kode yang tidak benar adalah kode yang buruk, bahkan lebih jika meskipun salah itu "berfungsi dengan baik", atau setidaknya sepertinya berfungsi dengan baik. Setelah itu, kode harus dapat dibaca (dibaca oleh seseorang yang tidak terbiasa dengannya).
Sedangkan untuk mengoptimalkan ... seseorang tentu tidak seharusnya secara sengaja menulis kode anti-dioptimalkan, dan tentu saja saya tidak mengatakan Anda tidak boleh menghabiskan waktu memikirkan desain sebelum Anda mulai (seperti memilih algoritma yang tepat untuk masalah tersebut, bukan yang paling tidak efisien).
Tetapi untuk sebagian besar aplikasi, sebagian besar waktu, kinerja yang Anda dapatkan setelah menjalankan kode yang benar dan dapat dibaca menggunakan algoritma yang masuk akal melalui kompiler pengoptimalisasi baik-baik saja, tidak perlu khawatir.
Jika itu tidak terjadi, yaitu jika kinerja aplikasi memang tidak memenuhi persyaratan, dan hanya itu , Anda harus khawatir tentang melakukan optimasi lokal seperti yang Anda coba. Namun, lebih disukai, Anda akan mempertimbangkan kembali algoritma tingkat atas. Jika Anda memanggil fungsi 500 kali alih-alih 50.000 kali karena algoritme yang lebih baik, ini memiliki dampak yang lebih besar daripada menyimpan tiga siklus clock pada optimasi mikro. Jika Anda tidak berhenti selama beberapa ratus siklus pada akses memori acak sepanjang waktu, ini memiliki dampak yang lebih besar daripada melakukan beberapa perhitungan murah ekstra, dll.
Optimalisasi adalah hal yang sulit (Anda dapat menulis seluruh buku tentang itu dan tidak ada habisnya), dan menghabiskan waktu untuk secara membabi buta mengoptimalkan beberapa tempat tertentu (bahkan tanpa mengetahui apakah itu hambatannya!) Biasanya menghabiskan waktu. Tanpa profil, pengoptimalan sangat sulit dilakukan.
Tetapi sebagai aturan praktis, ketika Anda terbang buta dan hanya perlu / ingin melakukan sesuatu , atau sebagai strategi standar umum, saya akan menyarankan untuk mengoptimalkan untuk "memori".
Mengoptimalkan "memori" (khususnya lokalitas spasial dan pola akses) biasanya menghasilkan manfaat karena tidak seperti dulu ketika semuanya "agak sama", saat ini mengakses RAM adalah salah satu hal yang paling mahal (singkat membaca dari disk!) yang pada prinsipnya dapat Anda lakukan. Sedangkan ALU, di sisi lain, murah dan semakin cepat setiap minggu. Bandwidth dan latensi memori tidak meningkat hampir secepat. Lokalitas yang baik dan pola akses yang baik dapat dengan mudah membuat perbedaan 5x (20x dalam contoh ekstrim, contrieved) dalam runtime dibandingkan dengan pola akses yang buruk dalam aplikasi data-berat. Bersikap baik terhadap cache Anda, dan Anda akan menjadi orang yang bahagia.
Untuk menempatkan paragraf sebelumnya ke dalam perspektif, pertimbangkan hal-hal berbeda apa yang dapat Anda lakukan untuk Anda. Menjalankan sesuatu seperti
a+b
mengambil (jika tidak dioptimalkan keluar) satu atau dua siklus, tetapi CPU biasanya dapat memulai beberapa instruksi per siklus, dan dapat menyalurkan instruksi yang tidak tergantung sehingga lebih realistis hanya biaya Anda sekitar setengah siklus atau kurang. Idealnya, jika kompiler bagus dalam penjadwalan, dan tergantung pada situasinya, mungkin harganya nol.Mengambil data ("memori") dikenakan biaya 4-5 siklus jika Anda beruntung dan berada di L1, dan sekitar 15 siklus jika Anda tidak seberuntung itu (hit L2). Jika data tidak ada dalam cache sama sekali, dibutuhkan beberapa ratus siklus. Jika pola akses serampangan Anda melebihi kemampuan TLB (mudah dilakukan hanya dengan ~ 50 entri), tambahkan beberapa ratus siklus lagi. Jika pola akses serampangan Anda benar-benar menyebabkan kesalahan halaman, Anda perlu beberapa ribu siklus dalam kasus terbaik, dan beberapa juta dalam kondisi terburuk.
Sekarang pikirkanlah, hal apa yang paling ingin Anda hindari?
sumber
Setelah mendapatkan fungsionalitas dengan benar terlebih dahulu . Kemudian selektivitas menyangkut diri dengan optimasi mikro.
Sebagai pertanyaan wawancara mengenai pengoptimalan, kode tersebut memancing diskusi yang biasa tetapi melewatkan tujuan tingkat yang lebih tinggi dari Apakah kode secara fungsional benar?
Baik C ++ dan C dan yang lainnya menganggap
int
overflow sebagai masalah daria + b
. Itu tidak didefinisikan dengan baik dan C menyebutnya perilaku tidak terdefinisi . Tidak ditentukan untuk "membungkus" - meskipun itu adalah perilaku umum.Fungsi seperti
IsSumInRange()
itu diharapkan akan didefinisikan dengan baik dan berkinerja dengan benar untuk semuaint
nilaia,b
. Yang mentaha + b
bukan. Solusi AC dapat menggunakan:Kode di atas dapat dioptimalkan dengan menggunakan tipe integer yang lebih luas daripada
int
, jika tersedia, seperti di bawah ini atau mendistribusikansum > N
,sum < -N
tes dalamif (a >= 0)
logika. Namun optimasi seperti itu mungkin tidak benar-benar mengarah pada kode yang dipancarkan "lebih cepat" yang diberikan kompiler pintar atau tidak layak pemeliharaan ekstra menjadi pintar.Bahkan menggunakan
abs(sum)
rentan terhadap masalah saatsum == INT_MIN
.sumber
Kompiler macam apa yang kita bicarakan, dan "memori" seperti apa? Karena dalam contoh Anda, dengan asumsi pengoptimal yang masuk akal, ekspresi
a+b
perlu umumnya disimpan dalam register (bentuk memori) sebelum melakukan aritmatika tersebut.Jadi jika kita berbicara tentang kompiler bodoh yang bertemu
a+b
dua kali, itu akan mengalokasikan lebih banyak register (memori) dalam contoh kedua Anda , karena contoh pertama Anda mungkin hanya menyimpan ekspresi itu sekali dalam satu register tunggal yang dipetakan ke variabel lokal, tapi kami Sedang berbicara tentang kompiler sangat konyol pada saat ini ... kecuali jika Anda bekerja dengan jenis kompiler konyol lain yang menumpahkan setiap variabel tunggal di semua tempat, dalam hal ini mungkin yang pertama akan menyebabkan lebih banyak kesedihan untuk dioptimalkan daripada kedua*.Ini semua sangat spekulatif dengan cara yang konyol, tidak ada pengukuran / pembongkaran dan bahkan dalam skenario terburuk, ini bukan kasus "memori vs kinerja" (karena bahkan di antara pengoptimal terburuk yang dapat saya pikirkan, kita tidak berbicara tentang apa pun kecuali memori sementara seperti tumpukan / daftar), ini murni kasus "kinerja" yang terbaik, dan di antara pengoptimal yang masuk akal keduanya sama, dan jika seseorang tidak menggunakan pengoptimal yang masuk akal, mengapa terobsesi tentang pengoptimalan jadi sifatnya mikroskopis dan pengukuran terutama absen? Itu seperti pemilihan tingkat alokasi instruksi / register alokasi yang tidak akan pernah saya harapkan ada orang yang ingin tetap produktif ketika menggunakan, katakanlah, seorang juru bahasa yang menumpahkan semuanya.
Adapun pertanyaan ini jika saya bisa mengatasinya secara lebih luas, sering saya tidak menemukan keduanya bertentangan. Terutama jika pola akses Anda berurutan, dan mengingat kecepatan cache CPU, sering kali pengurangan jumlah byte yang diproses secara berurutan untuk input non-sepele diterjemahkan (hingga titik) untuk membajak data itu lebih cepat. Tentu saja ada titik-titik putus di mana jika data jauh, jauh lebih kecil dalam pertukaran cara, lebih banyak instruksi, mungkin lebih cepat untuk memproses secara berurutan dalam bentuk yang lebih besar dengan imbalan instruksi yang lebih sedikit.
Tapi saya telah menemukan banyak pengembang cenderung meremehkan berapa banyak pengurangan dalam penggunaan memori dalam kasus-kasus semacam ini dapat diterjemahkan ke pengurangan proporsional dalam waktu yang dihabiskan pemrosesan. Sangat intuitif secara manusiawi untuk menerjemahkan biaya kinerja ke instruksi daripada akses memori ke titik pencapaian LUT besar dalam beberapa upaya sia-sia untuk mempercepat beberapa perhitungan kecil, hanya untuk menemukan kinerja terdegradasi dengan akses memori tambahan.
Untuk kasus akses berurutan melalui beberapa array besar (tidak berbicara variabel skalar lokal seperti dalam contoh Anda), saya mengikuti aturan bahwa lebih sedikit memori untuk membajak secara berurutan diterjemahkan menjadi kinerja yang lebih besar, terutama ketika kode yang dihasilkan lebih sederhana daripada sebaliknya, sampai tidak sampai pengukuran dan profiler saya mengatakan sebaliknya, dan itu penting, dengan cara yang sama saya menganggap secara berurutan membaca file biner yang lebih kecil pada disk akan lebih cepat untuk membajak daripada yang lebih besar (bahkan jika yang lebih kecil memerlukan beberapa instruksi lebih lanjut) ), sampai asumsi itu terbukti tidak berlaku lagi dalam pengukuran saya.
sumber