Thay vì suy đoán về những gì có thể hoặc không thể xảy ra, chúng ta hãy xem xét, phải không? Tôi sẽ phải sử dụng C ++ vì tôi không có trình biên dịch C # tiện dụng (mặc dù xem ví dụ C # từ VisualMelon ), nhưng tôi chắc chắn các nguyên tắc tương tự được áp dụng bất kể.
Chúng tôi sẽ bao gồm hai lựa chọn bạn gặp trong cuộc phỏng vấn. Chúng tôi cũng sẽ bao gồm một phiên bản sử dụng abs
theo đề xuất của một số câu trả lời.
#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);
}
Bây giờ biên dịch nó mà không cần tối ưu hóa gì: g++ -c -o test.o test.cpp
Bây giờ chúng ta có thể thấy chính xác những gì nó tạo ra: 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
Chúng ta có thể thấy từ các địa chỉ ngăn xếp (ví dụ: -0x4
in mov %edi,-0x4(%rbp)
so với -0x14
in mov %edi,-0x14(%rbp)
) IsSumInRangeWithVar()
sử dụng 16 byte bổ sung trên ngăn xếp.
Bởi vì IsSumInRangeWithoutVar()
phân bổ không có không gian trên ngăn xếp để lưu trữ giá trị trung gian, s
nó phải tính toán lại nó, dẫn đến việc thực hiện này là 2 hướng dẫn lâu hơn.
Hài hước, IsSumInRangeSuperOptimized()
trông rất giống IsSumInRangeWithoutVar()
, ngoại trừ so với -1000 đầu tiên và 1000 giây.
Bây giờ hãy biên dịch chỉ với các tối ưu hóa cơ bản nhất : g++ -O1 -c -o test.o test.cpp
. Kết quả:
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
Bạn sẽ nhìn vào đó: mỗi biến thể là giống hệt nhau . Trình biên dịch có thể làm một cái gì đó khá thông minh: abs(a + b) <= 1000
tương đương với a + b + 1000 <= 2000
việc xem xét setbe
thực hiện một so sánh không dấu, vì vậy một số âm trở thành một số dương rất lớn. Các lea
hướng dẫn thực sự có thể thực hiện tất cả những bổ sung trong một hướng dẫn, và loại bỏ tất cả các chi nhánh có điều kiện.
Để trả lời câu hỏi của bạn, hầu như luôn luôn điều tối ưu hóa không phải là bộ nhớ hay tốc độ, mà là khả năng đọc . Đọc mã khó hơn rất nhiều so với viết mã và đọc mã được đọc sai để "tối ưu hóa" khó hơn rất nhiều so với đọc mã được viết rõ ràng. Thường xuyên hơn không, những "tối ưu hóa" này không đáng kể, hoặc trong trường hợp này chính xác là không có tác động thực sự đến hiệu suất.
Theo dõi câu hỏi, những gì thay đổi khi mã này trong một ngôn ngữ diễn giải thay vì biên dịch? Sau đó, việc tối ưu hóa có vấn đề hay nó có kết quả tương tự?
Hãy đo! Tôi đã sao chép các ví dụ sang 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)),))
Chạy với Python 3.5.2, điều này tạo ra đầu ra:
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
Việc tháo gỡ trong Python không thú vị lắm, vì "trình biên dịch" mã byte không làm được gì nhiều trong cách tối ưu hóa.
Hiệu suất của ba chức năng gần như giống hệt nhau. Chúng tôi có thể bị cám dỗ để đi với IsSumInRangeWithVar()
do tăng tốc độ cận biên của nó. Mặc dù tôi sẽ thêm khi tôi đang thử các tham số khác nhau timeit
, đôi khi IsSumInRangeSuperOptimized()
xuất hiện nhanh nhất, vì vậy tôi nghi ngờ đó có thể là yếu tố bên ngoài chịu trách nhiệm cho sự khác biệt, thay vì bất kỳ lợi thế nội tại nào của việc triển khai.
Nếu đây thực sự là mã hiệu suất quan trọng, một ngôn ngữ được hiểu đơn giản là một lựa chọn rất kém. Chạy cùng một chương trình với pypy, tôi nhận được:
IsSumInRangeWithVar: 0.000180s
IsSumInRangeWithoutVar: 0.001175s
IsSumInRangeSuperOptimized: 0.001306s
Chỉ cần sử dụng pypy, sử dụng trình biên dịch JIT để loại bỏ rất nhiều chi phí phiên dịch, đã mang lại sự cải thiện hiệu suất của 1 hoặc 2 bậc độ lớn. Tôi đã khá sốc khi thấy IsSumInRangeWithVar()
một thứ tự cường độ nhanh hơn những thứ khác. Vì vậy, tôi đã thay đổi thứ tự của điểm chuẩn và chạy lại:
IsSumInRangeSuperOptimized: 0.000191s
IsSumInRangeWithoutVar: 0.001174s
IsSumInRangeWithVar: 0.001265s
Vì vậy, có vẻ như nó không thực sự là bất cứ điều gì về việc thực hiện nhanh chóng, mà là thứ tự tôi thực hiện điểm chuẩn!
Tôi muốn tìm hiểu sâu hơn về vấn đề này, vì thật lòng tôi không biết tại sao điều này lại xảy ra. Nhưng tôi tin rằng vấn đề đã được thực hiện: tối ưu hóa vi mô như có nên khai báo một giá trị trung gian là một biến hay không hiếm khi có liên quan. Với một ngôn ngữ được giải thích hoặc trình biên dịch được tối ưu hóa cao, mục tiêu đầu tiên vẫn là viết mã rõ ràng.
Nếu tối ưu hóa hơn nữa có thể được yêu cầu, điểm chuẩn . Hãy nhớ rằng các tối ưu hóa tốt nhất không đến từ các chi tiết nhỏ mà là bức tranh thuật toán lớn hơn: pypy sẽ là một thứ tự cường độ nhanh hơn để đánh giá lặp lại cùng chức năng so với cpython vì nó sử dụng thuật toán nhanh hơn (trình biên dịch JIT so với giải thích) để đánh giá chương trình. Và cũng có thuật toán được mã hóa để xem xét: tìm kiếm thông qua cây B sẽ nhanh hơn danh sách được liên kết.
Sau khi đảm bảo bạn đang sử dụng các công cụ và thuật toán phù hợp cho công việc, hãy chuẩn bị để đi sâu vào chi tiết của hệ thống. Kết quả có thể rất đáng ngạc nhiên, ngay cả đối với các nhà phát triển có kinh nghiệm và đây là lý do tại sao bạn phải có một điểm chuẩn để định lượng các thay đổi.