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.
#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, -0x4
in mov %edi,-0x4(%rbp)
versus the -0x14
in mov %edi,-0x14(%rbp)
) yang IsSumInRangeWithVar()
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 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) <= 1000
sama dengan a + b + 1000 <= 2000
mempertimbangkan setbe
melakukan perbandingan yang tidak ditandatangani, sehingga angka negatif menjadi angka positif yang sangat besar. The lea
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.
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.