Замість того, щоб міркувати про те, що може чи не може статися, давайте просто подивимось, чи не так? Мені доведеться використовувати C ++, оскільки у мене немає компілятора C # під рукою (хоча див. Приклад C # від VisualMelon ), але я впевнений, що ті самі принципи застосовуються незалежно.
Ми включимо дві альтернативи, з якими ви стикалися в інтерв'ю. Ми також включимо версію, яка використовує, abs
як пропонують деякі відповіді.
#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);
}
Тепер компілюйте його без жодної оптимізації: g++ -c -o test.o test.cpp
Тепер ми можемо точно бачити, що це породжує: 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
Ми бачимо з адрес стека (наприклад, -0x4
в mov %edi,-0x4(%rbp)
порівнянні з -0x14
in mov %edi,-0x14(%rbp)
), який IsSumInRangeWithVar()
використовує 16 додаткових байтів у стеці.
Оскільки IsSumInRangeWithoutVar()
в стеку не виділяється місця для зберігання проміжного значення, s
він повинен перерахувати його, внаслідок чого ця реалізація на 2 інструкції довша.
Смішно, IsSumInRangeSuperOptimized()
виглядає дуже схоже IsSumInRangeWithoutVar()
, за винятком того, що він порівнює -1000 перших та 1000 секунд.
Тепер давайте компілювати тільки найосновніші оптимізації: g++ -O1 -c -o test.o test.cpp
. Результат:
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
Ви подивитесь на це: кожен варіант однаковий . Компілятор здатний зробити щось досить розумне: abs(a + b) <= 1000
еквівалент a + b + 1000 <= 2000
розгляду setbe
робить порівняння без підпису, тому від'ємне число стає дуже великим додатним числом. lea
Інструкція може фактично виконувати всі ці доповнення в одній команді, і усунути всі умовні переходи.
Щоб відповісти на ваше запитання, майже завжди для оптимізації потрібна не пам’ять чи швидкість, а читабельність . Читати код набагато складніше, ніж його писати, а читання коду, який було налаштовано для оптимізації, набагато складніше, ніж читання коду, написаного, щоб бути зрозумілим. Найчастіше ці "оптимізації" мають незначний, або, як в цьому випадку, точно нульовий фактичний вплив на продуктивність.
Подальше запитання, що змінюється, коли цей код замість компілюється в інтерпретованій мові? Тоді, чи має значення оптимізація чи має такий же результат?
Давайте виміряємо! Я переписав приклади на 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)),))
Запуск із Python 3.5.2, це дає результат:
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
Розбирання в Python не дуже цікаво, оскільки байткод "компілятор" не дуже допомагає оптимізувати.
Продуктивність цих трьох функцій майже однакова. Ми можемо спокуситись піти IsSumInRangeWithVar()
через граничне збільшення швидкості. Хоча я додаю, що я намагався різні параметри timeit
, іноді IsSumInRangeSuperOptimized()
виходив найшвидше, тому я підозрюю, що за різницю можуть бути причиною зовнішні фактори, а не будь-яка внутрішня перевага будь-якої реалізації.
Якщо це справді критичний для продуктивності код, інтерпретована мова - це просто дуже поганий вибір. Запускаючи ту ж програму з pypy, я отримую:
IsSumInRangeWithVar: 0.000180s
IsSumInRangeWithoutVar: 0.001175s
IsSumInRangeSuperOptimized: 0.001306s
Просто використання pypy, що використовує компіляцію JIT, щоб усунути багато накладних витрат інтерпретатора, дало поліпшення продуктивності на 1 або 2 порядки. Я був дуже шокований, побачивши IsSumInRangeWithVar()
, що на порядок швидше, ніж інші. Тож я змінив порядок орієнтирів і знову побіг:
IsSumInRangeSuperOptimized: 0.000191s
IsSumInRangeWithoutVar: 0.001174s
IsSumInRangeWithVar: 0.001265s
Тож здається, що насправді це нічого не стосується впровадження, а швидше, в якому порядку я роблю тестування!
Я хотів би поглибитись до цього глибше, бо, чесно кажучи, не знаю, чому це відбувається. Але я вважаю, що справа зроблена: мікрооптимізація на зразок оголошення проміжного значення змінною чи ні, рідко є актуальною. З інтерпретованою мовою або високо оптимізованим компілятором першою метою є все-таки написати чіткий код.
Якщо може знадобитися додаткова оптимізація, орієнтир . Пам’ятайте, що найкращі оптимізації виходять не з дрібних деталей, а з більшої алгоритмічної картини: pypy буде на порядок швидшим для повторного оцінювання тієї ж функції, ніж cpython, оскільки для швидшого використання алгоритмів (JIT-компілятор проти інтерпретації) використовується програма. І тут також слід врахувати кодований алгоритм: пошук через B-дерево буде швидшим, ніж пов'язаний список.
Переконавшись , що ви використовуєте правильні інструменти і алгоритми для роботи, бути готовим до занурення глибоко в деталі системи. Результати можуть бути дуже дивовижними навіть для досвідчених розробників, і саме тому ви повинні мати орієнтир для кількісної оцінки змін.