Коли оптимізувати для пам'яті проти швидкості роботи метод?


107

Нещодавно я брав інтерв'ю в Amazon. Під час сеансу кодування інтерв'юер запитав, чому я оголосив змінну в методі. Я пояснив свій процес, і він кинув мені виклик вирішити ту саму проблему з меншою кількістю змінних. Наприклад (це було не з інтерв'ю), я почав із методу А, потім вдосконалив його до методу В, видаливши int s. Він був задоволений і сказав, що це зменшить використання пам'яті цим методом.

Я розумію логіку, яка стоїть за цим, але моє запитання:

Коли доцільно використовувати метод A проти методу B, і навпаки?

Ви можете бачити, що метод A матиме більше використання пам'яті, оскільки int sоголошується, але він повинен виконати лише один обчислення, тобто a + b. З іншого боку, метод B має менший обсяг пам'яті, але повинен виконувати два обчислення, тобто a + bдвічі. Коли я використовую одну техніку над іншою? Або один із прийомів завжди віддається перевазі іншому? Які речі слід враховувати при оцінці двох методів?

Спосіб A:

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

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

Спосіб B:

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

229
Я готовий зробити ставку, що сучасний компілятор створить однакову збірку для обох цих випадків.
17 з 26

12
Я повернув це питання до початкового стану, оскільки ваша редакція визнала мою відповідь недійсною - будь ласка, не робіть цього! Якщо ви ставите запитання, як поліпшити свій код, тоді не змінюйте питання, покращуючи код показаним способом - це робить відповіді безглуздими.
Док Браун

76
Почекайте секунду, вони попросили позбутися int s, будучи абсолютно чудовими з цими магічними цифрами для верхньої та нижньої межі?
null

34
Пам'ятайте: профіль перед оптимізацією. У сучасних компіляторах метод A і метод B можуть бути оптимізовані до одного і того ж коду (використовуючи більш високі рівні оптимізації). Також із сучасними процесорами вони можуть мати інструкції, які виконують більше, ніж додавання за одну операцію.
Томас Меттьюз

142
Ні; оптимізувати для читабельності.
Енді

Відповіді:


148

Замість того, щоб міркувати про те, що може чи не може статися, давайте просто подивимось, чи не так? Мені доведеться використовувати 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)порівнянні з -0x14in 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-дерево буде швидшим, ніж пов'язаний список.

Переконавшись , що ви використовуєте правильні інструменти і алгоритми для роботи, бути готовим до занурення глибоко в деталі системи. Результати можуть бути дуже дивовижними навіть для досвідчених розробників, і саме тому ви повинні мати орієнтир для кількісної оцінки змін.


6
Щоб навести приклад в C #: SharpLab створює однаковий ASM для обох методів (Desktop CLR v4.7.3130.00 (clr.dll) на x86)
VisualMelon

2
@VisualMelon досить приваблива позитивна перевірка: "return (((a + b)> = -1000) && ((a + b) <= 1000));" дає інший результат. : sharplab.io/…
Пітер Б

12
Читання може потенційно полегшити програму і для оптимізації. Компілятор може легко переписати для використання еквівалентної логіки, як це було вище, лише якщо він насправді зможе зрозуміти, що ви намагаєтеся зробити. Якщо ви використовуєте багато старовинних шкільних програм , передавайте між вкладками та покажчиками, використовуйте повторно змінне сховище тощо, компілятору може бути набагато складніше довести, що перетворення еквівалентне, і він просто залишить те, що ви написали , яка може бути неоптимальною.
Левшенко

1
@Corey див. Редагувати.
Філ Мороз

2
@Corey: ця відповідь насправді говорить вам саме те, що я написав у своїй відповіді: немає різниці, коли ви використовуєте гідний компілятор, а натомість зосереджуєтесь на readibilty. Звичайно, це виглядає краще обґрунтовано - можливо, ти мені зараз повіриш.
Док Браун

67

Щоб відповісти на поставлене запитання:

Коли оптимізувати для пам'яті проти швидкості роботи метод?

Ви повинні встановити дві речі:

  • Що обмежує вашу заявку?
  • Де я можу відновити більшу частину цього ресурсу?

Щоб відповісти на перше запитання, ви повинні знати, які вимоги щодо ефективності вашої заявки. Якщо немає вимог до продуктивності, то немає підстав для оптимізації того чи іншого шляху. Вимоги до продуктивності допомагають вам дістатися до місця "досить хорошого".

Метод, який ви надали самостійно, сам по собі не викликав би проблем із ефективністю, але, можливо, в циклі та обробці великої кількості даних вам доведеться трохи по-іншому думати про те, як ви підходите до проблеми.

Виявлення того, що обмежує додаток

Почніть переглядати поведінку вашої програми за допомогою монітора продуктивності. Слідкуйте за використанням процесора, диска, мережі та пам'яті під час роботи. Один або декілька предметів будуть виведені з максимуму, тоді як все інше буде використано помірковано - якщо тільки ви не досягнете ідеального балансу, але це майже ніколи не відбувається).

Коли вам потрібно заглянути глибше, зазвичай ви користуєтеся профілером . Є профілі пам'яті та профілі процесів , і вони вимірюють різні речі. Акт профілювання має значний вплив на продуктивність, але ви встановлюєте свій код, щоб з’ясувати, що не так.

Скажімо, ви бачите, що ваш процесор і диски досяг максимального використання. Спочатку ви перевірите наявність «гарячих точок» або коду, який або викликається частіше, ніж інші, або займає значно більший відсоток обробки.

Якщо ви не можете знайти жодних гарячих точок, ви б почали дивитися на пам'ять. Можливо, ви створюєте більше об'єктів, ніж потрібно, і ваше сміття працює понаднормово.

Відновлення виступу

Думайте критично. Наступний перелік змін - це порядок того, яку суму прибутку ви отримаєте:

  • Архітектура: шукайте точки комунікації
  • Алгоритм: спосіб обробки даних може знадобитися змінити
  • Гарячі точки: мінімізація частоти виклику гарячої точки може призвести до великого бонусу
  • Мікрооптимізація: це не є звичайним явищем, але іноді вам дійсно потрібно думати про незначні зміни (на зразок прикладу, який ви надали), особливо якщо це гаряча точка у вашому коді.

У таких ситуаціях вам доведеться застосовувати науковий метод. Придумайте гіпотезу, внесіть зміни та протестуйте її. Якщо ви досягаєте своїх цілей ефективності, ви закінчите. Якщо ні, перейдіть до наступного в списку.


Відповідь на питання жирним шрифтом:

Коли доцільно використовувати метод A проти методу B, і навпаки?

Чесно кажучи, це останній крок у спробі вирішити проблеми з продуктивністю чи пам'яттю. Вплив методу A проти методу B буде дійсно різним, залежно від мови та платформи (в деяких випадках).

Практично будь-яка компільована мова з напівдорого пристойним оптимізатором створить аналогічний код з будь-якою з цих структур. Однак ці припущення не обов'язково залишаються вірними у власних іграшкових мовах, які не мають оптимізатора.

Саме те, що матиме кращий вплив, залежить від того, чи sumє змінною стека чи змінною купи. Це вибір мовної реалізації. Наприклад, у C, C ++ та Java, число примітивів, таких як an, intє типовими змінними. Ваш код не має більшого впливу на пам'ять, призначивши змінну стека, ніж ви мали б з повністю вбудованим кодом.

Інші оптимізації, які ви можете знайти в бібліотеках С (особливо старіші), де вам доведеться вирішити між копіюванням двовимірного масиву вниз першим або впоперек, - це оптимізація, що залежить від платформи. Це вимагає певних знань про те, як чіпсет, на який ви орієнтуєтеся, найкраще оптимізує доступ до пам'яті. Між архітектурами існують тонкі відмінності.

Підсумок полягає в тому, що оптимізація - це поєднання мистецтва та науки. Це вимагає критичного мислення, а також певної міри гнучкості в тому, як ви підходите до проблеми. Шукайте великих речей, перш ніж звинувачувати в дрібних речах.


2
Ця відповідь найбільше фокусується на моєму питанні і не зациклюється на моїх прикладах кодування, тобто методі А та методі Б.
Кращий бюджет

18
Я відчуваю, що це загальна відповідь на тему "Як ви вирішуєте проблеми з ефективністю", але вам буде важко визначити відносне використання пам'яті від певної функції, виходячи з того, чи було у неї 4 або 5 змінних за допомогою цього методу. Я також сумніваюся, наскільки відповідний цей рівень оптимізації, коли компілятор (або інтерпретатор) може або не може оптимізувати це.
Ерік

@Eric, як я вже згадував, останньою категорією підвищення продуктивності будуть ваші мікрооптимізації. Єдиний спосіб гадати, чи це матиме якийсь вплив, - це виміряти продуктивність / пам'ять у профілері. Рідко такі поліпшення мають виграш, але при чутливих до часу проблемах з роботою у тренажерах є кілька добре поміщених змін, які можуть бути різницею між потраплянням вашої мети на час і ні. Я думаю, що я можу з однієї сторони рахувати кількість разів, які окупилися за 20 років роботи над програмним забезпеченням, але це не нуль.
Берін Лорич

@BerinLoritsch Знову ж таки, загалом я з вами згоден, але в цьому конкретному випадку я цього не роблю. Я дав власну відповідь, але особисто не бачив жодних інструментів, які б позначали або навіть давали вам способи потенційно визначити проблеми з продуктивністю, пов’язані з розміром стека пам'яті функції.
Ерік

@DocBrown, я це виправив. Щодо другого питання, я з вами майже згоден.
Берін Лорич

45

"це зменшило б пам'ять" - em, ні. Навіть якщо це було б правдою (що для будь-якого пристойного компілятора немає), різниця, швидше за все, була б незначною для будь-якої реальної ситуації в світі.

Однак я б рекомендував використовувати метод A * (метод A з незначною зміною):

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

але з двох абсолютно різних причин:

  • даючи змінній sпояснювальну назву, код стає зрозумілішим

  • це дозволяє уникнути того, що в коді двічі буде однакова логіка підсумовування, тому код стає більш ДУХОМ, що означає менше помилок, схильних до змін.


36
Я б очистив його ще далі і пішов з "поверненням суми> -1000 && сума <1000;".
17 з 26

36
@Corey будь-який гідний оптимізатор використовуватиме регістр процесора для sumзмінної, що призводить до нульового використання пам'яті. І навіть якщо ні, це лише одне слово пам’яті методом «листочка». Зважаючи на те, наскільки неймовірно марно витрачається на пам'ять Java або C # може бути інакше завдяки їх GC та об'єктній моделі, локальна intзмінна буквально не використовує помітної пам'яті. Це безглузда мікрооптимізація.
амон

10
@Corey: якщо це " трохи складніше", воно, ймовірно, не стане "помітним використанням пам'яті". Можливо, якщо ви побудуєте дійсно складніший приклад, але це робить це іншим питанням. Зауважте також, що оскільки ви не створюєте певну змінну для виразу, для складних проміжних результатів середовище часу виконання може все ще створювати тимчасові об'єкти, тому це повністю залежить від деталей мови, оточення, рівня оптимізації та що б ви не називали "помітним".
Док Браун

8
На додаток до наведених вище пунктів, я майже впевнений, що C # / Java вирішить зберігати, sumце буде детальною інформацією про реалізацію, і я сумніваюся, що хтось міг би зробити переконливий випадок щодо того, чи може дурна хитрість, як уникнення одного локального int, призведе до цього цей обсяг пам'яті в довгостроковій перспективі. Зрозумілість IMO важливіша. Читання може бути суб’єктивним, але FWIW, особисто я вважаю за краще, щоб ви ніколи не робили одне і те ж обчислення двічі, не для використання процесора, а тому, що мені доведеться лише один раз перевірити ваш додаток, коли я шукаю помилку.
jrh

2
... також зауважте, що мови, зібрані сміттям, взагалі є непередбачуваним, "бурхливим морем пам'яті", яке (для C # у будь-якому випадку) можна прибирати лише при необхідності. Я пам'ятаю, що склав програму, яка виділяла гігабайти оперативної пам'яті, і вона лише почалася " прибирання "після себе, коли пам'ять стала мізерною. Якщо GC не потрібно запускати, це може зайняти солодкий час і зберегти ваш процесор для більш нагальних питань.
jrh

35

Ви можете зробити краще, ніж обидва з

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

Більшість процесорів (і, отже, компілятори) можуть виконувати abs () за одну операцію. У вас не тільки менше сум, але і менше порівнянь, які, як правило, дорожче обчислювально. Він також видаляє розгалуження, що набагато гірше для більшості процесорів, оскільки воно перестає бути можливим у конвеєрному трубопроводі.

Як говорили інші відповіді, інтерв'юер - це рослинне життя і не має бізнесу, яке б проводило технічне інтерв'ю.

Однак, його питання справедливе. І відповідь на те, коли ви оптимізуєте і як, це коли ви довели, що це потрібно, і ви профілювали його, щоб довести, які саме частини потрібні . Кнут чудово сказав, що передчасна оптимізація - корінь усякого зла, оскільки надто легко намагатися сформувати неважливі розділи із золотої тарілки або внести зміни (на зразок вашого інтерв'юера), які не мають ніякого ефекту, при цьому пропускаючи місця, які справді потребують цього. Поки ви не отримаєте вагомих доказів, це дійсно необхідно, чіткість коду є важливішою ціллю.

Правка FabioTurati правильно вказує, що це протилежний логічний сенс до оригіналу (моя помилка!), І що це ілюструє подальший вплив цитати Кнута, де ми ризикуємо порушити код, намагаючись оптимізувати його.


2
@Corey, я цілком впевнений, що Грехем звертається із проханням "він кинув мені виклик вирішити ту саму проблему з меншими змінними", як очікувалося. Якщо я був би інтерв'юер, я б очікувати , що відповідь, нерухомо a+bв ifі робити це двічі. Ви неправильно розумієте: "Він був задоволений і сказав, що це зменшить використання пам'яті цим методом" - він був приємний до вас, приховуючи своє розчарування цим безглуздим поясненням пам'яті. Вам не слід сприймати серйозно, щоб тут ставити питання. Ви влаштувались на роботу? Думаю, ви цього не зробили :-(
Сінатр

1
Ви застосовуєте одночасно 2 перетворення: ви перетворили 2 умови на 1, використовуючи abs(), а також у вас є одна return, замість того, щоб мати одну, коли умова справжня ("якщо гілка") та ще одну, коли вона помилкова ( "інша гілка"). Коли ви змінюєте такий код, будьте обережні: є ризик ненароком записати функцію, яка повертає істину, коли вона повинна повернути помилкову, і навпаки. Саме це і сталося тут. Я знаю, що ти зосередився на іншій справі, і ти добре зробив це. Але все-таки це могло б легко коштувати вам роботи ...
Фабіо Турати

2
@FabioTurati Добре помічений - спасибі! Я оновлю відповідь. І це хороший момент щодо рефакторингу та оптимізації, що робить цитату Кнут ще більш актуальною. Ми повинні довести, що нам потрібна оптимізація, перш ніж ризикувати.
Грем

2
Більшість процесорів (і, отже, компілятори) можуть виконувати abs () за одну операцію. На жаль, це не стосується цілих чисел. ARM64 має умовний заперечення, який він може використовувати, якщо прапори вже встановлені з adds, і ARM має передбачуваний reverse-sub ( rsblt= reverse-sub, якщо менше tha), але все інше потребує декількох додаткових інструкцій для впровадження abs(a+b)або abs(a). godbolt.org/z/Ok_Con показує вихід ашми x86, ARM, AArch64, PowerPC, MIPS та RISC-V. Лише перетворюючи порівняння в діапазон, перевіряйте, (unsigned)(a+b+999) <= 1998Uчи може gcc оптимізувати його, як у відповіді Філа.
Пітер Кордес

2
"Удосконалений" код у цій відповіді все ще помиляється, оскільки він дає іншу відповідь IsSumInRange(INT_MIN, 0). Початковий код повертається falseтому INT_MIN+0 > 1000 || INT_MIN+0 < -1000, що ; але "новий і вдосконалений" код повертається trueтому, що abs(INT_MIN+0) < 1000. (Або в деяких мовах це
призведе

16

Коли доцільно використовувати метод A проти методу B, і навпаки?

Обладнання дешеве; програмісти дорогі . Тож вартість часу, який ви двоє витратили на це питання, напевно, набагато гірша, ніж будь-яка відповідь.

Незважаючи на те, більшість сучасних компіляторів знайдуть спосіб оптимізувати локальну змінну в регістр (замість виділення місця у стеку), тому методи, ймовірно, однакові з точки зору виконуваного коду. З цієї причини більшість розробників обрали б варіант, який найбільш чітко повідомляє про наміри (див. Написання дійсно очевидного коду (ROC) ). На мою думку, це був би метод А.

З іншого боку, якщо це суто академічні вправи, ви можете мати найкращі з обох світів за допомогою методу C:

private bool IsSumInRange(int a, int b)
{
    a += b;
    return (a >= -1000 && a <= 1000);
}

17
a+=bце акуратний трюк, але я мушу зазначити (про всяк випадок, коли це не випливає з решти відповіді), з мого досвіду методи, які псують параметри, можуть бути дуже важкими для налагодження та підтримки.
jrh

1
Я згоден @jrh. Я твердий прихильник РПЦ, і така річ - це що інше.
Джон Ву

3
"Апаратне забезпечення дешеве; програмісти дорогі." У світі побутової електроніки це твердження хибне. Якщо ви продаєте мільйони одиниць, то це дуже гарна інвестиція - витратити 500 000 доларів на додаткові витрати на розробку, щоб заощадити 0,10 доларів на апаратні витрати на одиницю.
Барт ван Іґен Шенау

2
@JohnWu: Ви спростили ifперевірку, але забули відмінити результат порівняння; тепер ваша функція повертається, trueколи a + bїї немає в діапазоні. Або додайте в !умову зовнішню умову ( return !(a > 1000 || a < -1000)), або розподіліть !інвертуючі тести, щоб отримати return a <= 1000 && a >= -1000;Або зробити так, щоб діапазон перевірки діапазону був прекрасним,return -1000 <= a && a <= 1000;
ShadowRanger

1
@JohnWu: все-таки трохи не в крайніх випадках, розподілена логіка вимагає <=/ >=, а не </ ></ >, 1000 і -1000 трактуються як поза діапазоном, оригінальний код поводиться з ними як у діапазоні).
ShadowRanger

11

Я б оптимізував для читабельності. Спосіб 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);
}

Невеликі методи, які роблять лише одну річ, але їх легко розсудити.

(Це особисті переваги. Мені подобається позитивне тестування замість негативного; ваш оригінальний код насправді перевіряє, чи значення НЕ знаходиться поза діапазоном.)


5
Це. (Опубліковані вище коментарі, подібні до читабельності). 30 років тому, коли ми працювали з машинами, що мали менше 1 Мбайт оперативної пам’яті, необхідне скорочення продуктивності - як і проблема y2k, отримайте кілька сотень тисяч записів про те, що кожен з них витрачає кілька байтів пам’яті через невикористані витрати та посилання тощо, і це швидко додається, коли у вас є лише 256 кб оперативної пам’яті. Тепер, коли ми маємо справу з машинами, що мають кілька гігабайт оперативної пам’яті, економлячи навіть кілька МБ оперативної пам’яті та читабельність та ремонтопридатність коду - це не надто хороша торгівля.
іваніван

@ivanivan: Я не думаю, що "проблема y2k" стосувалася пам'яті. З точки зору введення даних, введення двох цифр ефективніше, ніж введення чотирьох, а зберігати введені речі простіше, ніж конвертувати їх у якусь іншу форму.
supercat

10
Тепер вам потрібно простежити дві функції, щоб побачити, що відбувається. Ви не можете прийняти це за номінал, тому що не можете від імені сказати, чи це все включені чи ексклюзивні межі. І якщо ви додасте цю інформацію, ім'я функції довше коду, щоб висловити її.
Пітер

1
Оптимізуйте читабельність і зробіть невеликі, прості в розумі функції - впевнені, згодні. Але я категорично НЕ згоден , що перейменовувати aі bдо number1і number2посібники читаність в будь-якому випадку. Крім того, ваше іменування функцій суперечливо: чому IsSumInRangeжорстко кодує діапазон, якщо IsValueInRangeприймає його як аргументи?
близько

1-а функція може переповнюватися. (Як і код інших відповідей.) Хоча складність переповнення безпечного коду є аргументом для введення його у функцію.
філіпсі

6

Коротше кажучи, я не думаю, що питання має велику актуальність у сучасних обчисленнях, але з історичної точки зору це цікава думка.

Ваш інтерв'юер, ймовірно, шанувальник Міфічного місяця людини. У книзі Фред Брукс стверджує, що програмістам, як правило, потрібні дві версії ключових функцій у своїй панелі інструментів: версія, орієнтована на пам'ять та версія, оптимізована процесором. Фред грунтується на цьому на своєму досвіді, який веде розробку операційної системи IBM System / 360, де машини можуть мати всього 8 кілобайт оперативної пам’яті. У таких машинах потенційно може бути важливою пам'ять, необхідна для локальних змінних функцій, особливо якщо компілятор не ефективно їх оптимізував (або якщо код був записаний безпосередньо мовою складання).

У поточну епоху, я думаю, вам важко буде знайти систему, де наявність чи відсутність локальної змінної у методі помітно зміниться. Для змінного значення метод повинен бути рекурсивним, очікується глибока рекурсія. Вже тоді, ймовірно, глибина стека буде перевищена, викликаючи винятки переповнення стека, перш ніж сама змінна викликала проблему. Єдиний реальний сценарій, коли це може бути проблемою, - це дуже великі масиви, виділені на стеку рекурсивним методом. Але це також малоймовірно, оскільки я думаю, що більшість розробників подумає двічі про непотрібні копії великих масивів.


4

Після завдання s = a + b; змінні a і b більше не використовуються. Тому для s не використовується пам'ять, якщо ви не використовуєте повністю пошкоджений мозку компілятор; пам'ять, яка все одно була використана для a і b, використовується повторно.

Але оптимізація цієї функції - суцільна нісенітниця. Якщо ви могли б заощадити простір, це було б може бути 8 байт під час роботи функції (яка відновлюється, коли функція повертається), так абсолютно безглуздо. Якби ви могли заощадити час, це були б одиничні числа наносекунд. Оптимізація цього є загальною тратою часу.


3

Змінні типу місцевого значення виділяються на стеку або (що швидше для таких невеликих фрагментів коду) використовують регістри в процесорі і ніколи не отримують побачити жодну оперативну пам'ять. У будь-якому випадку вони недовговічні і ні про що не турбуватися. Ви починаєте розглянути можливість використання пам’яті, коли вам потрібно створити буфер чи чергувати елементи даних у колекціях, які є потенційно великими та довгоживучими.

Тоді залежить від того, що ви найбільше піклуєтесь про вашу заявку. Швидкість обробки? Час реакції? Слід пам’яті? Технічне обслуговування? Послідовність в дизайні? Все залежить від вас.


4
Nitpicking: принаймні .NET (мова публікації не визначена) не дає жодних гарантій щодо виділення локальних змінних "у стеку". Дивіться "стек - деталь реалізації" Еріка Ліпперта.
jrh

1
@jrh Локальні змінні в стеці чи купі можуть бути детальною інформацією про реалізацію, але якщо хтось дійсно хотів змінної в стеці є stackallocі зараз Span<T>. Можливо корисно в гарячій точці, після профілювання. Крім того, деякі документи навколо структур означають, що типи значень можуть бути в стеку, тоді як посилальних типів не буде. У будь-якому випадку, у кращому випадку ви можете уникнути трохи GC.
Боб

2

Як говорили інші відповіді, вам потрібно подумати, для чого ви оптимізуєте.

У цьому прикладі я підозрюю, що будь-який гідний компілятор генерував би еквівалентний код для обох методів, тому рішення не матиме впливу на час виконання та пам'ять!

Що ж впливає на це читаність коду. (Кодекс призначений для того, щоб люди читали, а не лише комп'ютери.) Немає великої різниці між двома прикладами; коли всі інші речі рівні, я вважаю стислість доброчесністю, тому, мабуть, я б обрав метод Б. Але всі інші речі рідко рівні, і в більш складному випадку реального світу це може мати великий ефект.

Що слід врахувати:

  • Чи має проміжне вираження якісь побічні ефекти? Якщо він викликає будь-які нечисті функції або оновлює будь-які змінні, то, звичайно, дублювання це було б питанням правильності, а не просто стилю.
  • Наскільки складним є проміжний вираз? Якщо він виконує багато обчислень та / або функцій викликів, компілятор, можливо, не зможе його оптимізувати, і це вплине на продуктивність. (Хоча, як сказав Кнут , "ми повинні забути про малу ефективність, скажімо, про 97% часу".)
  • Чи має проміжна змінна якесь значення ? Чи можна назвати ім’я, яке допоможе пояснити, що відбувається? Коротка, але інформативна назва могла б пояснити код краще, тоді як безглузда - лише візуальний шум.
  • Як довго проміжний вираз? Якщо тривалий, то його дублювання може зробити код довшим і важчим для читання (особливо, якщо це змушує розрив рядка); якщо ні, то дублювання могло б бути коротшим за всіх.

1

Як було зазначено у багатьох відповідях, спроба налаштувати цю функцію за допомогою сучасних компіляторів не матиме жодних значень. Оптимізатор, швидше за все, може знайти найкраще рішення (голосувати до відповіді, яка показала код асемблера, щоб довести це!). Ви заявляли, що код інтерв'ю не є саме тим кодом, який вас попросили порівняти, тому, можливо, власне приклад має трохи більше сенсу.

Але давайте поглянемо ще на це питання: це питання інтерв'ю. Отже, справжнє питання полягає в тому, як слід на це відповісти, припускаючи, що ви хочете спробувати влаштуватися на роботу?

Припустимо також, що інтерв'юер знає, про що вони говорять, і вони просто намагаються побачити те, що ви знаєте.

Я зазначу, що, ігноруючи оптимізатор, перший може створити тимчасову змінну на стеці, тоді як другий не буде, але виконає обчислення двічі. Тому перший використовує більше пам’яті, але швидше.

Ви можете згадати, що в будь-якому випадку для обчислення може знадобитися тимчасова змінна для зберігання результату (щоб її порівняти), тож чи називати ви цю змінну чи ні, це може не мати ніякої різниці.

Тоді я б зазначив, що насправді код буде оптимізований і, швидше за все, буде створений еквівалентний машинний код, оскільки всі змінні є локальними. Однак це залежить від того, який компілятор ви використовуєте (не так давно я міг отримати корисне підвищення продуктивності, оголосивши локальну змінну як "остаточну" на Java).

Ви можете згадати, що стек у будь-якому випадку живе на своїй власній сторінці пам'яті, тому, якщо ваша додаткова змінна не призведе до того, що стек переповнює сторінку, він насправді не виділяє більше пам'яті. Якщо це все-таки переповнюється, хочете отримати зовсім нову сторінку.

Я хотів би зазначити, що більш реалістичним прикладом може бути вибір того, використовувати кеш-пам'ять для зберігання результатів багатьох обчислень чи ні, і це поставить питання про процесор проти пам’яті.

Все це свідчить про те, що ви знаєте, про що говорите.

Я хотів би сказати, що краще зосередитись на читанні. Хоча це правда в цьому випадку, в контексті інтерв'ю це може трактуватися як "Я не знаю про продуктивність, але мій код звучить як історія Джанет та Джона ".

Те, що ви не повинні робити, - це вимкнути звичайні непристойні твердження про те, як оптимізація коду не потрібна, не оптимізуйте, поки ви не профілірували код (це просто означає, що ви не можете бачити поганий код для себе), вартість обладнання менше, ніж програмісти , і, будь ласка, не цитуйте Кнут "передчасний бла-бла ...".

Виконання коду є справжньою проблемою у багатьох організаціях, і багатьом організаціям потрібні програмісти, які це розуміють.

Зокрема, для таких організацій, як Amazon, деякий код має величезні важелі. Фрагмент коду може бути розгорнутий на тисячі серверів або мільйонів пристроїв і може називатися мільярдами разів на день кожен день року. Можуть бути тисячі подібних фрагментів. Різниця між поганим алгоритмом і хорошим може легко бути коефіцієнтом тисячі. Зробіть числа і помножте все це вгору: це має значення. Потенційна вартість організації неефективного коду може бути дуже істотною або навіть фатальною, якщо у системи не вистачає потужностей.

Надалі, багато з цих організацій працюють у конкурентному середовищі. Таким чином, ви не можете просто сказати своїм клієнтам придбати більший комп’ютер, якщо програмне забезпечення вашого конкурента вже працює нормально на апаратному забезпеченні, яке вони мають, або якщо програмне забезпечення працює на мобільній трубці, і його неможливо оновити. Деякі програми особливо важливі для роботи (ігри та мобільні додатки приходять у голову) і можуть жити чи вмирати відповідно до їх чутливості чи швидкості.

Я особисто протягом двох десятиліть працював над багатьма проектами, де системи вийшли з ладу або були непридатними через проблеми з продуктивністю, і мене викликали в оптимізації цих систем, і в усіх випадках це було через поганий код, написаний програмістами, які не розуміли вплив того, про що вони писали. Далі, це ніколи не один шматок коду, він завжди є скрізь. Коли я під’їжджаю, вже пізно починати думати про продуктивність: шкода була завдана.

Розуміння продуктивності коду - це хороша майстерність, як і розуміння правильності коду та стилю коду. Це виходить із практики. Збої в роботі можуть бути такими ж поганими, як і функціональні збої. Якщо система не працює, вона не працює. Не має значення чому. Так само продуктивність та функції, які ніколи не використовуються, є поганими.

Отже, якщо інтерв'юер запитає вас про ефективність роботи, я б рекомендував спробувати продемонструвати якомога більше знань. Якщо питання здається поганим, ввічливо вкажіть, чому ви вважаєте, що це не буде проблемою в такому випадку. Не цитуйте Кнут.


0

Спочатку слід оптимізувати правильність.

Ваша функція не працює для вхідних значень, близьких до Int.MaxValue:

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

Це повертає істину, оскільки сума переповнюється до -400. Функція також не працює для a = int.MinValue + 200. (неправильно додається до "400")

Ми не будемо знати, що шукав інтерв'юер, якщо він чи вона не задзвенить, але "переповнення справжнє" .

У ситуації співбесіди задайте питання, щоб уточнити масштаб проблеми: Що таке допустимі максимальні та мінімальні вхідні значення? Коли ви їх отримаєте, ви можете скинути виняток, якщо абонент подає значення за межами діапазону. Або (у C #) ви можете скористатися відміченою {} секцією, яка б накинула виняток на переповнення. Так, це більше робота і складна, але іноді це потрібно.


Методи були лише прикладами. Вони написані не для правильності, а для ілюстрації актуального питання. Дякую за вклад, хоча!
Кращий бюджет

Я думаю, що питання інтерв'ю спрямоване на виконання, тому вам потрібно відповісти про наміри питання. Інтерв'юер не питає про поведінку в межах меж. Але цікавий бічний момент все одно.
rghome

1
@Corey Хороші інтерв'ю як запитання до 1) оцінюють здатність кандидата щодо цього питання, як це запропонував rghome, але також 2) як відкриття до більш великих питань (на зразок невимовної функціональної коректності) та глибини відповідних знань - це тим більше у пізніших інтерв'ю про кар’єру - удача.
chux

0

Ваше питання повинно було бути: "Чи потрібно взагалі оптимізувати це?".

Версії A і B відрізняються однією важливою деталлю, яка робить A кращим, але він не пов'язаний з оптимізацією: Ви не повторюєте код.

Фактичною «оптимізацією» називається загальне усунення підвыражения, саме це робить кожен компілятор. Деякі роблять цю основну оптимізацію навіть тоді, коли оптимізація вимкнена. Тож це не справді оптимізація (згенерований код майже напевно буде абсолютно однаковим у кожному випадку).

Але якщо це не оптимізація, то чому вона є кращою? Гаразд, ви не повторите код, кому все одно!

Ну, по-перше, у вас немає ризику випадкового помилки половини умовного пункту. Але ще важливіше, що хтось, читаючи цей код, може негайно шукати те, що ви намагаєтесь, замість if((((wtf||is||this||longexpression))))досвіду. Що читач бачить - if(one || theother)це добре. Не рідко трапляється, що ви є тим, хто через три роки читає ваш власний код і думає, що "WTF це означає?". У такому випадку завжди корисно, якщо ваш код негайно повідомляє про те, що було наміром. Якщо звичайна субэкспресія названа належним чином, це так.
Крім того , якщо в будь-який час в майбутньому, ви вирішили , що , наприклад , вам потрібно змінити , a+bщоб a-b, ви повинні змінити однумісце розташування, а не два. І немає жодного ризику (знову) випадково помилитися з другим.

Щодо власного питання, для чого слід оптимізувати, перш за все, ваш код повинен бути правильним . Це абсолютно найголовніше. Неправильний код - це поганий код, тим більше, що, незважаючи на неправильність, він "працює нормально", або принаймні виглядає, що він працює добре. Після цього код повинен бути читабельним (читабельним для когось незнайомим).
Щодо оптимізації ... звичайно, не слід свідомо писати антиоптимізований код, і, звичайно, я не кажу, що не варто витрачати думки на розробку перед початком роботи (наприклад, вибір правильного алгоритму для проблеми, не найменш ефективний).

Але для більшості додатків більшу частину часу продуктивність, яку ви отримуєте після запуску правильного, читабельного коду за допомогою розумного алгоритму за допомогою оптимізуючого компілятора, просто чудова, справді не потрібно хвилюватися.

Якщо це не так, тобто якщо продуктивність програми дійсно не відповідає вимогам, і тільки тоді , вам слід потурбуватися про те, щоб зробити таку локальну оптимізацію, як та, яку ви намагалися. Однак, переважно, ви б переглянули алгоритм верхнього рівня. Якщо ви викликаєте функцію 500 разів замість 50 000 разів через кращий алгоритм, це має більший вплив, ніж економія трьох тактових циклів на мікрооптимізації. Якщо ви не зупиняєтесь на кілька сотень циклів на випадковому доступі до пам'яті весь час, це має більший вплив, ніж робити кілька додаткових дешевих обчислень, тощо, тощо.

Оптимізація - справа непроста (ви можете написати цілі книги про це і добігти кінця), а витрачати час на сліпу оптимізацію якогось певного місця (навіть не знаючи, чи це взагалі вузьке місце!), Як правило, даремно витрачається час. Без профілювання оптимізацію дуже важко отримати правильно.

Але, як правило, коли ви летите сліпими і просто потрібно / хочете щось зробити , або як загальну стратегію за замовчуванням, я б запропонував оптимізувати для "пам'яті".
Оптимізація «пам’яті» (зокрема просторової локальності та шаблонів доступу) зазвичай приносить користь, оскільки на відміну від колись, коли все було «якось однаково», сьогодні доступ до оперативної пам’яті є одним з найдорожчих речей (не вистачає читання з диска!) що ви в принципі можете зробити. Тоді як ALU, з іншого боку, дешевий і швидший з кожним тижнем. Пропускна здатність і затримка пам'яті не покращуються майже так швидко. Хороша локальність та хороші шаблони доступу можуть легко змінити 5-кратну різницю (20-кратний у крайніх, надуманих прикладах) в режимі виконання порівняно з поганими моделями доступу у важких додатках. Будьте приємні до своїх схованок, і ви будете щасливою людиною.

Щоб поставити попередній абзац в перспективу, подумайте, що коштують вам різні речі, які ви можете зробити. Виконання чогось подібного a+bзаймає (якщо не оптимізовано) один або два цикли, але процесор зазвичай може запускати кілька інструкцій за цикл, і може розробити незалежні інструкції настільки реалістичніше, що це коштуватиме вам щось близько півциклу або менше. В ідеалі, якщо компілятор хороший у плануванні, і залежно від ситуації, це може коштувати нуля.
Отримання даних ("пам'ять") коштує вам 4-5 циклів, якщо вам пощастить, і це в L1, і близько 15 циклів, якщо вам не так пощастило (L2 попадання). Якщо даних взагалі немає в кеші, потрібно кілька сотень циклів. Якщо ваш випадковий шаблон доступу перевищує можливості TLB (це легко зробити лише з ~ 50 записів), додайте ще кілька сотень циклів. Якщо ваш випадковий шаблон доступу насправді спричиняє помилку на сторінці, це коштує вам в декількох випадках десять тисяч циклів, а в гіршому - кілька мільйонів.
А тепер подумайте над тим, що ви хочете уникнути найбільш терміново?


0

Коли оптимізувати для пам'яті проти швидкості роботи метод?

Після отримання функціональності право першого . Тоді вибірковість стосується мікрооптимізацій.


Що стосується питання інтерв'ю щодо оптимізації, код викликає звичайну дискусію, проте не вистачає мети вищого рівня: Чи функціонально правильний код?

І C ++, і C та інші розглядають intпереповнення як проблему з a + b. Він недостатньо визначений, і С називає це невизначеною поведінкою . Не вказується "загортати" - хоча це звичайна поведінка.

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

IsSumInRange()Очікується, що така функція, яка називається, буде чітко визначена та виконана правильно для всіх intзначень a,b. Сирого a + bнемає. Розчин змінного струму може використовувати:

#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;
}

Вище код може бути оптимізовано за допомогою більш широкого , ніж цілочисельний тип int, якщо такі є, як показано нижче , або розподіляючи sum > N, sum < -Nтести в межах if (a >= 0)логіки. Однак такі оптимізації можуть по-справжньому не призвести до «швидшого» випромінюваного коду, що дається розумним компілятором, і не варто додаткового обслуговування бути розумним.

  long long sum a;
  sum += b;

Навіть використання abs(sum)схильне до проблем, коли sum == INT_MIN.


0

Про які компілятори ми говоримо і про яку «пам’ять»? Тому що у вашому прикладі, припускаючи розумний оптимізатор, вираз a+bпотрібно, як правило, зберігати в регістрі (формі пам'яті) перед тим, як виконувати таку арифметику.

Отже, якщо ми говоримо про тупий компілятор, який стикається a+bдвічі, він виділить більше регістрів (пам'яті) у вашому другому прикладі, тому що ваш перший приклад може просто зберігати це вираз один раз в одному регістрі, зіставленому на локальну змінну, але ми У цей момент ви говорите про дуже нерозумні компілятори ... якщо ви не працюєте з іншим типом нерозумного компілятора, який стек розливає кожну змінну всюди, і, можливо, перший може спричинити оптимізацію більше горя, ніж секунда*.

Я все ще хочу це почухати, і думаю, що другий, швидше за все, буде використовувати більше пам’яті з німим компілятором, навіть якщо він схильний до a+bрозсипання стека, тому що це може врешті-решт виділити три регістри для та розливу aта bінше. Якщо ми говоримо найпримітивніший оптимізатор , то захопивши a+bз s, ймовірно , «допомога» він був менше регістрів стека / розливи.

Це все надзвичайно спекулятивно, бо досить дурні способи відсутні вимірювання / розбирання і навіть у найгірших сценаріях це не випадок "пам'ять проти продуктивності" (адже навіть серед найгірших оптимізаторів, про які я можу придумати, ми не говоримо про що-небудь, крім тимчасової пам’яті, як стек / регістр), це в кращому випадку справа «продуктивності», і серед будь-якого розумного оптимізатора два є рівнозначними, і якщо один не використовує розумний оптимізатор, то чому блукає оптимізація настільки мікроскопічна за своєю природою і особливо відсутні вимірювання? Це як вибір вибору інструкцій / реєстрування розподілу уваги на рівні складання, який я ніколи не очікував, що хтось, хто хоче залишатися продуктивним, матиме при використанні, скажімо, інтерпретатора, що стек проливає все.

Коли оптимізувати для пам'яті проти швидкості роботи метод?

Щодо цього питання, якщо я можу вирішити його ширше, часто я не знаходжу двох діаметрально протилежних. Особливо, якщо ваші шаблони доступу є послідовними, і враховуючи швидкість кешу процесора, часто зменшення кількості байтів, що обробляються послідовно для нетривіальних входів, переводить (до точки) на швидше орання цих даних. Звичайно, є точки розриву, коли, якщо дані набагато, набагато менші в обмін на спосіб, більше інструкцій, може бути швидше обробляти послідовно у більшому вигляді в обмін на меншу кількість інструкцій.

Але я виявив, що багато розробників, як правило, недооцінюють, наскільки зменшення використання пам'яті в таких типах випадків може призвести до пропорційного скорочення часу на обробку. Дуже по-людськи інтуїтивно перекладати витрати продуктивності на інструкції, а не на доступ до пам'яті до місця досягнення великих LUT в якійсь марній спробі прискорити невеликі обчислення, лише знайти продуктивність, погіршену з додатковим доступом до пам'яті.

Для випадків послідовного доступу через величезний масив (не кажучи про локальні скалярні змінні, як у вашому прикладі), я дію за правилом, що менше пам'яті, щоб послідовно переробляти, перетворюється на більшу продуктивність, особливо, коли отриманий код простіший, ніж інакше, поки він не стане 'т, поки мої вимірювання та профілер не скажуть мені інакше, і це має значення, таким же чином я припускаю, що послідовне читання меншого бінарного файлу на диску буде швидше оратись, ніж більший (навіть якщо менший вимагає ще декількох інструкцій ), поки не буде показано, що це припущення більше не застосовується в моїх вимірах.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.