Kapan untuk mengoptimalkan memori vs kecepatan kinerja untuk suatu metode?

107

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 sdideklarasikan, 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 + bdua 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;
}
Corey P
sumber
229
Saya berani bertaruh bahwa kompiler modern akan menghasilkan rakitan yang sama untuk kedua kasus tersebut.
17 dari 26
12
Saya mengembalikan pertanyaan ke keadaan semula, karena hasil edit Anda membatalkan jawaban saya - tolong jangan lakukan itu! Jika Anda mengajukan pertanyaan bagaimana meningkatkan kode Anda, maka jangan ubah pertanyaan dengan meningkatkan kode dengan cara yang ditunjukkan - ini membuat jawaban tampak tidak berarti.
Doc Brown
76
Tunggu sebentar, mereka meminta untuk menyingkirkan int ssementara benar-benar baik-baik saja dengan angka ajaib itu untuk batas atas dan bawah?
null
34
Ingat: profil sebelum mengoptimalkan. Dengan kompiler modern, Metode A dan Metode B dapat dioptimalkan ke kode yang sama (menggunakan level optimisasi yang lebih tinggi). Juga, dengan prosesor modern, mereka dapat memiliki instruksi yang melakukan lebih dari sekadar penambahan dalam satu operasi.
Thomas Matthews
142
Tidak juga; mengoptimalkan keterbacaan.
Andy

Jawaban:

148

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 absseperti yang disarankan oleh beberapa jawaban.

#include <cstdlib>

bool IsSumInRangeWithVar(int a, int b)
{
    int s = a + b;

    if (s > 1000 || s < -1000) return false;
    else return true;
}

bool IsSumInRangeWithoutVar(int a, int b)
{
    if (a + b > 1000 || a + b < -1000) return false;
    else return true;
}

bool IsSumInRangeSuperOptimized(int a, int b) {
    return (abs(a + b) < 1000);
}

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

0000000000000000 <_Z19IsSumInRangeWithVarii>:
   0:   55                      push   %rbp              # begin a call frame
   1:   48 89 e5                mov    %rsp,%rbp
   4:   89 7d ec                mov    %edi,-0x14(%rbp)  # save first argument (a) on stack
   7:   89 75 e8                mov    %esi,-0x18(%rbp)  # save b on stack
   a:   8b 55 ec                mov    -0x14(%rbp),%edx  # load a and b into edx
   d:   8b 45 e8                mov    -0x18(%rbp),%eax  # load b into eax
  10:   01 d0                   add    %edx,%eax         # add a and b
  12:   89 45 fc                mov    %eax,-0x4(%rbp)   # save result as s on stack
  15:   81 7d fc e8 03 00 00    cmpl   $0x3e8,-0x4(%rbp) # compare s to 1000
  1c:   7f 09                   jg     27                # jump to 27 if it's greater
  1e:   81 7d fc 18 fc ff ff    cmpl   $0xfffffc18,-0x4(%rbp) # compare s to -1000
  25:   7d 07                   jge    2e                # jump to 2e if it's greater or equal
  27:   b8 00 00 00 00          mov    $0x0,%eax         # put 0 (false) in eax, which will be the return value
  2c:   eb 05                   jmp    33 <_Z19IsSumInRangeWithVarii+0x33>
  2e:   b8 01 00 00 00          mov    $0x1,%eax         # put 1 (true) in eax
  33:   5d                      pop    %rbp
  34:   c3                      retq

0000000000000035 <_Z22IsSumInRangeWithoutVarii>:
  35:   55                      push   %rbp
  36:   48 89 e5                mov    %rsp,%rbp
  39:   89 7d fc                mov    %edi,-0x4(%rbp)
  3c:   89 75 f8                mov    %esi,-0x8(%rbp)
  3f:   8b 55 fc                mov    -0x4(%rbp),%edx
  42:   8b 45 f8                mov    -0x8(%rbp),%eax  # same as before
  45:   01 d0                   add    %edx,%eax
  # note: unlike other implementation, result is not saved
  47:   3d e8 03 00 00          cmp    $0x3e8,%eax      # compare to 1000
  4c:   7f 0f                   jg     5d <_Z22IsSumInRangeWithoutVarii+0x28>
  4e:   8b 55 fc                mov    -0x4(%rbp),%edx  # since s wasn't saved, load a and b from the stack again
  51:   8b 45 f8                mov    -0x8(%rbp),%eax
  54:   01 d0                   add    %edx,%eax
  56:   3d 18 fc ff ff          cmp    $0xfffffc18,%eax # compare to -1000
  5b:   7d 07                   jge    64 <_Z22IsSumInRangeWithoutVarii+0x2f>
  5d:   b8 00 00 00 00          mov    $0x0,%eax
  62:   eb 05                   jmp    69 <_Z22IsSumInRangeWithoutVarii+0x34>
  64:   b8 01 00 00 00          mov    $0x1,%eax
  69:   5d                      pop    %rbp
  6a:   c3                      retq

000000000000006b <_Z26IsSumInRangeSuperOptimizedii>:
  6b:   55                      push   %rbp
  6c:   48 89 e5                mov    %rsp,%rbp
  6f:   89 7d fc                mov    %edi,-0x4(%rbp)
  72:   89 75 f8                mov    %esi,-0x8(%rbp)
  75:   8b 55 fc                mov    -0x4(%rbp),%edx
  78:   8b 45 f8                mov    -0x8(%rbp),%eax
  7b:   01 d0                   add    %edx,%eax
  7d:   3d 18 fc ff ff          cmp    $0xfffffc18,%eax
  82:   7c 16                   jl     9a <_Z26IsSumInRangeSuperOptimizedii+0x2f>
  84:   8b 55 fc                mov    -0x4(%rbp),%edx
  87:   8b 45 f8                mov    -0x8(%rbp),%eax
  8a:   01 d0                   add    %edx,%eax
  8c:   3d e8 03 00 00          cmp    $0x3e8,%eax
  91:   7f 07                   jg     9a <_Z26IsSumInRangeSuperOptimizedii+0x2f>
  93:   b8 01 00 00 00          mov    $0x1,%eax
  98:   eb 05                   jmp    9f <_Z26IsSumInRangeSuperOptimizedii+0x34>
  9a:   b8 00 00 00 00          mov    $0x0,%eax
  9f:   5d                      pop    %rbp
  a0:   c3                      retq

Kita dapat melihat dari alamat stack (misalnya, -0x4in mov %edi,-0x4(%rbp)versus the -0x14in mov %edi,-0x14(%rbp)) yang IsSumInRangeWithVar()menggunakan 16 byte tambahan pada stack.

Karena IsSumInRangeWithoutVar()tidak mengalokasikan ruang pada tumpukan untuk menyimpan nilai menengah, sia harus menghitung ulang, sehingga implementasi ini menjadi 2 instruksi lebih lama.

Lucu, IsSumInRangeSuperOptimized()terlihat sangat mirip IsSumInRangeWithoutVar(), 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:

0000000000000000 <_Z19IsSumInRangeWithVarii>:
   0:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
   7:   3d d0 07 00 00          cmp    $0x7d0,%eax
   c:   0f 96 c0                setbe  %al
   f:   c3                      retq

0000000000000010 <_Z22IsSumInRangeWithoutVarii>:
  10:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
  17:   3d d0 07 00 00          cmp    $0x7d0,%eax
  1c:   0f 96 c0                setbe  %al
  1f:   c3                      retq

0000000000000020 <_Z26IsSumInRangeSuperOptimizedii>:
  20:   8d 84 37 e8 03 00 00    lea    0x3e8(%rdi,%rsi,1),%eax
  27:   3d d0 07 00 00          cmp    $0x7d0,%eax
  2c:   0f 96 c0                setbe  %al
  2f:   c3                      retq

Apakah Anda akan melihatnya: setiap varian identik . Kompiler dapat melakukan sesuatu yang cukup pintar: abs(a + b) <= 1000sama dengan a + b + 1000 <= 2000mempertimbangkan setbemelakukan perbandingan yang tidak ditandatangani, sehingga angka negatif menjadi angka positif yang sangat besar. The leainstruksi 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.


Pertanyaan tindak lanjut, apa yang berubah ketika kode ini dalam bahasa yang ditafsirkan alih-alih dikompilasi? Lalu, apakah optimasi itu penting atau apakah hasilnya sama?

Ayo ukur! Saya telah menyalin contoh ke Python:

def IsSumInRangeWithVar(a, b):
    s = a + b
    if s > 1000 or s < -1000:
        return False
    else:
        return True

def IsSumInRangeWithoutVar(a, b):
    if a + b > 1000 or a + b < -1000:
        return False
    else:
        return True

def IsSumInRangeSuperOptimized(a, b):
    return abs(a + b) <= 1000

from dis import dis
print('IsSumInRangeWithVar')
dis(IsSumInRangeWithVar)

print('\nIsSumInRangeWithoutVar')
dis(IsSumInRangeWithoutVar)

print('\nIsSumInRangeSuperOptimized')
dis(IsSumInRangeSuperOptimized)

print('\nBenchmarking')
import timeit
print('IsSumInRangeWithVar: %fs' % (min(timeit.repeat(lambda: IsSumInRangeWithVar(42, 42), repeat=50, number=100000)),))
print('IsSumInRangeWithoutVar: %fs' % (min(timeit.repeat(lambda: IsSumInRangeWithoutVar(42, 42), repeat=50, number=100000)),))
print('IsSumInRangeSuperOptimized: %fs' % (min(timeit.repeat(lambda: IsSumInRangeSuperOptimized(42, 42), repeat=50, number=100000)),))

Jalankan dengan Python 3.5.2, ini menghasilkan output:

IsSumInRangeWithVar
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD
              7 STORE_FAST               2 (s)

  3          10 LOAD_FAST                2 (s)
             13 LOAD_CONST               1 (1000)
             16 COMPARE_OP               4 (>)
             19 POP_JUMP_IF_TRUE        34
             22 LOAD_FAST                2 (s)
             25 LOAD_CONST               4 (-1000)
             28 COMPARE_OP               0 (<)
             31 POP_JUMP_IF_FALSE       38

  4     >>   34 LOAD_CONST               2 (False)
             37 RETURN_VALUE

  6     >>   38 LOAD_CONST               3 (True)
             41 RETURN_VALUE
             42 LOAD_CONST               0 (None)
             45 RETURN_VALUE

IsSumInRangeWithoutVar
  9           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD
              7 LOAD_CONST               1 (1000)
             10 COMPARE_OP               4 (>)
             13 POP_JUMP_IF_TRUE        32
             16 LOAD_FAST                0 (a)
             19 LOAD_FAST                1 (b)
             22 BINARY_ADD
             23 LOAD_CONST               4 (-1000)
             26 COMPARE_OP               0 (<)
             29 POP_JUMP_IF_FALSE       36

 10     >>   32 LOAD_CONST               2 (False)
             35 RETURN_VALUE

 12     >>   36 LOAD_CONST               3 (True)
             39 RETURN_VALUE
             40 LOAD_CONST               0 (None)
             43 RETURN_VALUE

IsSumInRangeSuperOptimized
 15           0 LOAD_GLOBAL              0 (abs)
              3 LOAD_FAST                0 (a)
              6 LOAD_FAST                1 (b)
              9 BINARY_ADD
             10 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             13 LOAD_CONST               1 (1000)
             16 COMPARE_OP               1 (<=)
             19 RETURN_VALUE

Benchmarking
IsSumInRangeWithVar: 0.019361s
IsSumInRangeWithoutVar: 0.020917s
IsSumInRangeSuperOptimized: 0.020171s

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 berbeda timeit, kadang IsSumInRangeSuperOptimized()- 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:

IsSumInRangeWithVar: 0.000180s
IsSumInRangeWithoutVar: 0.001175s
IsSumInRangeSuperOptimized: 0.001306s

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:

IsSumInRangeSuperOptimized: 0.000191s
IsSumInRangeWithoutVar: 0.001174s
IsSumInRangeWithVar: 0.001265s

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.

Phil Frost
sumber
6
Untuk memberikan contoh dalam C #: SharpLab menghasilkan asm identik untuk kedua metode (Desktop CLR v4.7.3130.00 (clr.dll) pada x86)
VisualMelon
2
@VisualMelon cukup lucu dengan cek positif: "return (((a + b)> = -1000) && ((a + b) <= 1000));" memberikan hasil yang berbeda. : sharplab.io/…
Pieter B
12
Keterbacaan berpotensi dapat membuat program lebih mudah untuk dioptimalkan juga. Kompiler dapat dengan mudah menulis ulang untuk menggunakan logika yang sama seperti di atas, hanya jika ia benar-benar dapat mengetahui apa yang Anda coba lakukan. Jika Anda menggunakan banyak bithacks old-school , dilemparkan bolak-balik antara int dan pointer, gunakan kembali penyimpanan yang dapat diubah, dll. Mungkin akan lebih sulit bagi kompiler untuk membuktikan bahwa transformasi setara, dan itu hanya akan meninggalkan apa yang Anda tulis , yang mungkin suboptimal.
Leushenko
1
@Corey lihat edit.
Phil Frost
2
@Corey: jawaban ini sebenarnya memberi tahu Anda apa yang saya tulis dalam jawaban saya: tidak ada perbedaan ketika Anda menggunakan kompiler yang layak, dan alih-alih fokus pada readibilty. Tentu saja, itu terlihat lebih mapan - mungkin Anda percaya padaku sekarang.
Doc Brown
67

Untuk menjawab pertanyaan yang disebutkan:

Kapan untuk mengoptimalkan memori vs kecepatan kinerja untuk suatu metode?

Ada dua hal yang harus Anda bangun:

  • Apa yang membatasi aplikasi Anda?
  • Di mana saya bisa mendapatkan kembali sebagian besar sumber daya itu?

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:

  • Arsitektur: mencari titik tersedak komunikasi
  • Algoritma: cara Anda memproses data mungkin perlu diubah
  • Hot spot: meminimalkan seberapa sering Anda menelepon hot spot dapat menghasilkan bonus besar
  • Optimalisasi mikro: itu tidak umum, tetapi kadang-kadang Anda benar-benar perlu memikirkan tweak kecil (seperti contoh yang Anda berikan), terutama jika itu adalah hot spot dalam kode Anda.

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:

Kapan tepat menggunakan Metode A vs Metode B, dan sebaliknya?

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 sumvariabel tumpukan atau variabel tumpukan. Ini adalah pilihan implementasi bahasa. Dalam C, C ++ dan Java misalnya, bilangan primitif seperti a intadalah 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.

Berin Loritsch
sumber
2
Jawaban ini berfokus pada pertanyaan saya yang paling dan tidak terjebak pada contoh pengkodean saya, yaitu Metode A dan Metode B.
Corey P
18
Saya merasa seperti ini adalah jawaban umum untuk "Bagaimana Anda mengatasi hambatan kinerja" tetapi Anda akan kesulitan untuk mengidentifikasi penggunaan memori relatif dari fungsi tertentu berdasarkan apakah ada 4 atau 5 variabel menggunakan metode ini. Saya juga mempertanyakan seberapa relevan tingkat optimisasi ini ketika kompiler (atau penerjemah) mungkin atau tidak mengoptimalkannya.
Eric
@ Eric, seperti yang saya sebutkan, kategori terakhir dari peningkatan kinerja adalah optimasi mikro Anda. Satu-satunya cara untuk memiliki tebakan yang baik jika itu akan berdampak adalah dengan mengukur kinerja / memori dalam profiler. Sangat jarang bahwa jenis-jenis perbaikan memiliki hasil, tetapi dalam masalah waktu masalah kinerja yang Anda miliki dalam simulator pasangan perubahan yang baik seperti itu bisa menjadi perbedaan antara memukul target waktu Anda dan tidak. Saya pikir saya dapat menghitung di satu sisi berapa kali terbayar dalam lebih dari 20 tahun bekerja pada perangkat lunak, tetapi itu bukan nol.
Berin Loritsch
@BerinLoritsch Sekali lagi, secara umum saya setuju dengan Anda, tetapi dalam kasus khusus ini saya tidak. Saya telah memberikan jawaban saya sendiri, tetapi saya pribadi tidak melihat alat apa pun yang akan menandai atau bahkan memberi Anda cara untuk mengidentifikasi masalah kinerja yang berpotensi terkait dengan ukuran memori dari suatu fungsi.
Eric
@DocBrown, saya telah memperbaikinya. Mengenai pertanyaan kedua, saya cukup setuju dengan Anda.
Berin Loritsch
45

"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):

private bool IsSumInRange(int a, int b)
{
    int sum = a + b;

    if (sum > 1000 || sum < -1000) return false;
    else return true;
    // (yes, the former statement could be cleaned up to
    // return abs(sum)<=1000;
    // but let's ignore this for a moment)
}

tetapi karena dua alasan yang sangat berbeda:

  • dengan memberikan variabel snama yang menjelaskan, kode menjadi lebih jelas

  • itu menghindari untuk memiliki logika penjumlahan yang sama dua kali dalam kode, sehingga kode menjadi lebih KERING, yang berarti lebih sedikit kesalahan rentan terhadap perubahan.

Doc Brown
sumber
36
Saya akan membersihkannya lebih jauh dan pergi dengan "jumlah pengembalian> -1000 && jumlah <1000;".
17 dari 26
36
@Corey pengoptimal yang layak akan menggunakan register CPU untuk sumvariabel, 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, intvariabel lokal secara harfiah tidak menggunakan memori yang terlihat. Ini adalah optimasi mikro yang tidak ada gunanya.
amon
10
@Corey: jika " sedikit lebih kompleks", itu mungkin tidak akan menjadi "penggunaan memori yang nyata". Mungkin jika Anda membuat contoh yang benar-benar lebih rumit, tetapi itu membuatnya menjadi pertanyaan yang berbeda. Perhatikan juga, hanya karena Anda tidak membuat variabel spesifik untuk ekspresi, untuk hasil perantara yang kompleks, lingkungan run time mungkin masih secara internal membuat objek sementara, jadi itu sepenuhnya tergantung pada detail bahasa, lingkungan, tingkat optimasi, dan apa pun yang Anda sebut "terlihat".
Doc Brown
8
Selain poin-poin di atas, saya cukup yakin bagaimana C # / Java memilih untuk menyimpan sumakan menjadi detail implementasi dan saya ragu ada yang bisa meyakinkan apakah ada trik konyol seperti menghindari satu lokal intakan 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.
jrh
2
... juga perhatikan bahwa sampah yang dikumpulkan bahasa secara umum adalah "lautan memori" yang tak terduga, yang (untuk C # tetap) hanya dapat dibersihkan bila diperlukan , saya ingat membuat program yang mengalokasikan gigabyte RAM dan baru dimulai " membersihkan "setelah dirinya sendiri ketika ingatan menjadi langka. Jika GC tidak perlu dijalankan, mungkin butuh waktu yang manis dan hemat CPU Anda untuk masalah yang lebih mendesak.
jrh
35

Anda dapat melakukan lebih baik dari keduanya

return (abs(a + b) > 1000);

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.

Graham
sumber
2
@Corey, saya cukup yakin permintaan pin Graham "dia menantang saya untuk memecahkan masalah yang sama dengan variabel kurang" seperti yang diharapkan. Jika saya menjadi pewawancara, saya mengharapkan jawaban itu, tidak pindah a+bke ifdan 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 :-(
Sinatr
1
Anda menerapkan 2 transformasi pada saat yang sama: Anda telah mengubah 2 kondisi menjadi 1, menggunakan abs(), dan Anda juga memiliki satu return, 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 Anda
Fabio Turati
2
@FabioTurati Terlihat dengan baik - terima kasih! Saya akan memperbarui jawabannya. Dan itu poin bagus tentang refactoring dan optimalisasi, yang membuat kutipan Knuth lebih relevan. Kita harus membuktikan bahwa kita membutuhkan pengoptimalan sebelum mengambil risiko.
Graham
2
Kebanyakan prosesor (dan karenanya kompiler) dapat melakukan abs () dalam satu operasi. Sayangnya tidak demikian untuk bilangan bulat. ARM64 memiliki persyaratan negate yang dapat digunakan jika flag sudah diatur dari adds, dan ARM telah memprediksikan reverse-sub ( rsblt= reverse-sub jika less-tha) tetapi yang lainnya membutuhkan beberapa instruksi tambahan untuk mengimplementasikan abs(a+b)atau abs(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) <= 1998Ubahwa gcc dapat mengoptimalkannya seperti dalam jawaban Phil.
Peter Cordes
2
Kode "yang ditingkatkan" dalam jawaban ini masih salah, karena menghasilkan jawaban yang berbeda untuk IsSumInRange(INT_MIN, 0). Kode asli kembali falsekarena INT_MIN+0 > 1000 || INT_MIN+0 < -1000; tetapi kode "baru dan lebih baik" kembali truekarena abs(INT_MIN+0) < 1000. (Atau, dalam beberapa bahasa, itu akan menimbulkan pengecualian atau memiliki perilaku yang tidak terdefinisi. Periksa daftar lokal Anda.)
Quuxplusone
16

Kapan tepat menggunakan Metode A vs Metode B, dan sebaliknya?

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:

private bool IsSumInRange(int a, int b)
{
    a += b;
    return (a >= -1000 && a <= 1000);
}
John Wu
sumber
17
a+=badalah 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.
jrh
1
Saya setuju @jrh. Saya seorang advokat yang kuat untuk ROC, dan hal-hal semacam itu sama sekali tidak.
John Wu
3
"Perangkat keras itu murah; programmer mahal." Dalam dunia elektronik konsumen, pernyataan itu salah. Jika Anda menjual jutaan unit, maka itu adalah investasi yang sangat baik untuk menghabiskan $ 500.000 dalam biaya pengembangan tambahan untuk menghemat $ 0,10 pada biaya perangkat keras per unit.
Bart van Ingen Schenau
2
@ JohnWu: Anda menyederhanakan ifcek, tetapi lupa membalikkan hasil perbandingan; fungsi Anda sekarang kembali trueketika a + bini tidak dalam kisaran. Tambahkan a !ke bagian luar kondisi ( return !(a > 1000 || a < -1000)), atau bagikan !tes, pembalik, untuk mendapatkan return a <= 1000 && a >= -1000;atau untuk membuat aliran rentang periksa dengan baik,return -1000 <= a && a <= 1000;
ShadowRanger
1
@ JohnWu: Masih sedikit tidak aktif pada kasus tepi, logika terdistribusi memerlukan <=/ >=, bukan </ >(dengan </ >, 1000 dan -1000 diperlakukan sebagai di luar jangkauan, kode asli memperlakukannya seperti dalam jangkauan).
ShadowRanger
11

Saya akan mengoptimalkan untuk keterbacaan. Metode X:

private bool IsSumInRange(int number1, int number2)
{
    return IsValueInRange(number1+number2, -1000, 1000);
}

private bool IsValueInRange(int Value, int Lowerbound, int Upperbound)
{
    return  (Value >= Lowerbound && Value <= Upperbound);
}

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.)

Pieter B
sumber
5
Ini. (Komentar terverifikasi di atas yang mirip dengan re: keterbacaan). 30 tahun yang lalu, ketika kami bekerja dengan mesin yang memiliki kurang dari 1mb RAM, memeras kinerja diperlukan - seperti masalah y2k, dapatkan beberapa ratus ribu catatan yang masing-masing memiliki beberapa byte memori yang terbuang karena vars yang tidak digunakan dan referensi, dll dan itu bertambah cepat ketika Anda hanya memiliki 256 ribu RAM. Sekarang kita berhadapan dengan mesin yang memiliki banyak RAM gigabytes, menghemat bahkan beberapa MB penggunaan RAM vs keterbacaan dan pemeliharaan kode bukan perdagangan yang baik.
ivanivan
@ivanivan: Saya tidak berpikir "masalah y2k" benar-benar tentang memori. Dari sudut pandang entri data, memasukkan dua digit lebih efisien daripada memasukkan empat, dan menjaga hal-hal yang dimasukkan lebih mudah daripada mengubahnya ke bentuk lain.
supercat
10
Sekarang Anda harus menelusuri 2 fungsi untuk melihat apa yang terjadi. Anda tidak dapat menerima nilai nominalnya, karena Anda tidak dapat mengatakan dari namanya apakah ini adalah batas inklusif atau eksklusif. Dan jika Anda menambahkan informasi itu, nama fungsinya lebih panjang daripada kode untuk mengungkapkannya.
Peter
1
Optimalkan keterbacaan dan buat fungsi kecil, mudah untuk alasan - pasti, setuju. Tapi saya sangat tidak setuju bahwa mengubah nama adan buntuk number1dan number2membantu keterbacaan dengan cara apa pun. Penamaan fungsi Anda juga tidak konsisten: mengapa IsSumInRangehard-code rentang jika IsValueInRangemenerimanya sebagai argumen?
leftaroundabout
Fungsi 1 dapat meluap. (Seperti kode jawaban lain.) Meskipun kompleksitas kode overflow-safe adalah argumen untuk memasukkannya ke dalam fungsi.
philipxy
6

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.

Eric
sumber
4

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.

gnasher729
sumber
3

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.

Martin Maat
sumber
4
Nitpicking: .NET setidaknya (bahasa posting tidak ditentukan) tidak membuat jaminan tentang variabel lokal yang dialokasikan "pada tumpukan". Lihat "tumpukan adalah detail implementasi" oleh Eric Lippert.
jrh
1
@ jrh Variabel lokal pada stack atau heap mungkin merupakan detail implementasi, tetapi jika seseorang benar-benar menginginkan variabel di stack ada stackallocdan sekarang Span<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.
Bob
2

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:

  • Apakah ekspresi perantara memiliki efek samping? Jika itu memanggil fungsi tidak murni atau memperbarui variabel apa pun, maka tentu saja menduplikasinya akan menjadi masalah kebenaran, bukan hanya gaya.
  • Seberapa kompleks ekspresi perantara? Jika melakukan banyak perhitungan dan / atau memanggil fungsi, maka kompiler mungkin tidak dapat mengoptimalkannya, dan ini akan mempengaruhi kinerja. (Padahal, seperti kata Knuth , "Kita harus melupakan efisiensi kecil, katakanlah sekitar 97% dari waktu").
  • Apakah variabel perantara memiliki arti ? Mungkinkah diberi nama yang membantu menjelaskan apa yang terjadi? Nama pendek tapi informatif bisa menjelaskan kode lebih baik, sedangkan yang tidak berarti hanyalah suara visual.
  • Berapa lama ekspresi menengah? Jika panjang, kemudian menduplikasinya dapat membuat kode lebih panjang dan lebih sulit untuk dibaca (terutama jika memaksa baris istirahat); jika tidak, duplikasi bisa lebih pendek dari semuanya.
gidds
sumber
1

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.

rghome
sumber
0

Anda harus mengoptimalkan dulu untuk kebenaran.

Fungsi Anda gagal untuk nilai input yang dekat dengan Int.MaxValue:

int a = int.MaxValue - 200;
int b = int.MaxValue - 200;
bool inRange = test.IsSumInRangeA(a, b);

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.

TomEberhard
sumber
Metode hanya contoh. Mereka tidak ditulis untuk menjadi benar, tetapi untuk menggambarkan pertanyaan yang sebenarnya. Terima kasih atas masukannya!
Corey P
Saya pikir pertanyaan wawancara diarahkan pada kinerja, jadi Anda harus menjawab maksud pertanyaan itu. Pewawancara tidak bertanya tentang perilaku pada batasnya. Tapi pokoknya menarik juga.
rghome
1
@Corey Pewawancara yang baik sebagai pertanyaan ke 1) menilai kemampuan kandidat mengenai masalah ini, seperti yang disarankan oleh rghome di sini juga 2) sebagai pembuka ke masalah yang lebih besar (seperti kebenaran fungsional yang tak terucapkan) dan kedalaman pengetahuan terkait - ini lebih dari itu dalam wawancara karier selanjutnya - semoga sukses.
chux
0

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 adalah if(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+bke a-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+bmengambil (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?

Damon
sumber
0

Kapan untuk mengoptimalkan memori vs kecepatan kinerja untuk suatu metode?

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 intoverflow sebagai masalah dari a + b. Itu tidak didefinisikan dengan baik dan C menyebutnya perilaku tidak terdefinisi . Tidak ditentukan untuk "membungkus" - meskipun itu adalah perilaku umum.

bool IsSumInRange(int a, int b) {
    int s = a + b;  // Overflow possible
    if (s > 1000 || s < -1000) return false;
    else return true;
}

Fungsi seperti IsSumInRange()itu diharapkan akan didefinisikan dengan baik dan berkinerja dengan benar untuk semua intnilai a,b. Yang mentah a + bbukan. Solusi AC dapat menggunakan:

#define N 1000
bool IsSumInRange_FullRange(int a, int b) {
  if (a >= 0) {
    if (b > INT_MAX - a) return false;
  } else {
    if (b < INT_MIN - a) return false;
  }
  int sum = a + b;
  if (sum > N || sum < -N) return false;
  else return true;
}

Kode di atas dapat dioptimalkan dengan menggunakan tipe integer yang lebih luas daripada int, jika tersedia, seperti di bawah ini atau mendistribusikan sum > N, sum < -Ntes dalam if (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.

  long long sum a;
  sum += b;

Bahkan menggunakan abs(sum)rentan terhadap masalah saat sum == INT_MIN.

chux
sumber
0

Kompiler macam apa yang kita bicarakan, dan "memori" seperti apa? Karena dalam contoh Anda, dengan asumsi pengoptimal yang masuk akal, ekspresi a+bperlu umumnya disimpan dalam register (bentuk memori) sebelum melakukan aritmatika tersebut.

Jadi jika kita berbicara tentang kompiler bodoh yang bertemu a+bdua 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*.

Saya masih ingin menggaruknya dan berpikir yang kedua kemungkinan akan menggunakan lebih banyak memori dengan kompiler bodoh bahkan jika itu cenderung untuk menumpahkan tumpahan, karena pada akhirnya mungkin mengalokasikan tiga register untuk a+bdan tumpah adan bbanyak lagi. Jika kita berbicara optimizer paling primitif kemudian menangkap a+bke smungkin akan "membantu" itu menggunakan register kurang / tumpahan stack.

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.

Kapan untuk mengoptimalkan memori vs kecepatan kinerja untuk suatu metode?

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.

Energi Naga
sumber