Когда оптимизировать память по сравнению с быстродействием метода?

107

Я недавно давал интервью на Amazon. Во время сеанса кодирования интервьюер спросил, почему я объявил переменную в методе. Я объяснил свой процесс, и он предложил мне решить ту же проблему с меньшим количеством переменных. Например (это было не из интервью), я начал с метода A, а затем улучшил его до метода B, удалив int s. Он был рад и сказал, что это уменьшит использование памяти этим методом.

Я понимаю логику, стоящую за этим, но мой вопрос:

Когда целесообразно использовать метод A против метода B, и наоборот?

Вы можете видеть , что метод А будет иметь более высокое использование памяти, так как int sдекларируется, но он имеет только выполнить один расчет, то есть a + b. С другой стороны, метод B использует меньше памяти, но должен выполнить два вычисления, то есть a + bдважды. Когда я использую одну технику над другой? Или один из методов всегда предпочтительнее другого? Что нужно учитывать при оценке двух методов?

Метод А:

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

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

Метод Б:

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то время как они полностью в порядке с этими магическими числами для верхних и нижних границ?
ноль
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

Мы можем видеть из стековых адресов (например, -0x4in 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
@ Смотрите см. Редактировать.
Фил Фрост
2
@Corey: этот ответ на самом деле говорит вам именно то, что я написал в своем ответе: нет разницы, когда вы используете достойный компилятор, а вместо этого сосредотачиваетесь на читаемости. Конечно, это выглядит более обоснованным - может быть, вы мне сейчас верите.
Док Браун
67

Чтобы ответить на поставленный вопрос:

Когда оптимизировать память по сравнению с быстродействием метода?

Есть две вещи, которые вы должны установить:

  • Что ограничивает ваше приложение?
  • Где я могу вернуть большую часть этого ресурса?

Чтобы ответить на первый вопрос, вы должны знать, каковы требования к производительности для вашего приложения. Если нет требований к производительности, то нет причин для оптимизации тем или иным способом. Требования к производительности помогут вам добраться до места «достаточно хорошо».

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

Обнаружение того, что ограничивает приложение

Начните смотреть на поведение вашего приложения с монитором производительности. Следите за использованием процессора, диска, сети и памяти во время работы. Один или несколько предметов будут максимально использованы, в то время как все остальное используется умеренно - если только вы не достигнете идеального баланса, но это почти никогда не происходит).

Когда вам нужно посмотреть глубже, обычно вы используете профилировщик . Есть профайлеры памяти и процесс профайлеры , и они измеряют разные вещи. Процесс профилирования оказывает существенное влияние на производительность, но вы используете свой код, чтобы выяснить, в чем дело.

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

Если вы не можете найти какие-либо горячие точки, вы бы начали смотреть на память. Возможно, вы создаете больше объектов, чем необходимо, и ваша сборка мусора работает сверхурочно.

Восстановление производительности

Думай критически. Ниже приведен список изменений в порядке окупаемости инвестиций:

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

В подобных ситуациях вы должны применять научный метод. Придумайте гипотезу, внесите изменения и проверьте ее. Если вы достигаете своих целей производительности, все готово. Если нет, перейдите к следующей вещи в списке.


Отвечая на вопрос, выделенный жирным шрифтом:

Когда целесообразно использовать метод A против метода B, и наоборот?

Честно говоря, это последний шаг в попытке справиться с проблемами производительности или памяти. Влияние метода A против метода B будет действительно различным в зависимости от языка и платформы (в некоторых случаях).

Почти любой скомпилированный язык с наполовину приличным оптимизатором будет генерировать аналогичный код с любой из этих структур. Однако эти предположения не обязательно остаются верными в проприетарных и игрушечных языках, в которых нет оптимизатора.

То, что будет иметь лучшее влияние, зависит от того, sumявляется ли переменная стека или переменной кучи. Это выбор языковой реализации. Например, в C, C ++ и Java числовые примитивы, такие как a, intявляются переменными стека по умолчанию. Ваш код не оказывает большего влияния на память при назначении переменной стека, чем при полностью встроенном коде.

Другие оптимизации, которые вы можете найти в библиотеках C (особенно в старых), где вы можете выбрать между копированием 2-мерного массива вниз или первым - это оптимизация, зависящая от платформы. Это требует определенных знаний о том, как выбранный вами чипсет лучше всего оптимизирует доступ к памяти. Есть тонкие различия между архитектурами.

Суть в том, что оптимизация - это сочетание искусства и науки. Это требует некоторого критического мышления, а также определенной гибкости в подходе к проблеме. Ищите большие вещи, прежде чем винить маленькие вещи.

Берин Лорич
источник
2
Этот ответ больше всего фокусируется на моем вопросе и не затрагивает мои примеры кодирования, т. Е. Метод A и метод B.
Corey P
18
Я чувствую, что это общий ответ на вопрос «Как вы решаете узкие места производительности», но вам будет сложно определить относительное использование памяти для конкретной функции на основе того, имеет ли она 4 или 5 переменных, используя этот метод. Я также задаюсь вопросом, насколько уместен этот уровень оптимизации, когда компилятор (или интерпретатор) может или не может оптимизировать это.
Эрик
@Eric, как я уже говорил, последней категорией повышения производительности будут ваши микрооптимизации. Единственный способ получить правильное предположение, окажет ли он какое-либо влияние, - это измерить производительность / память в профилировщике. Редко, когда улучшения такого типа приносят свои плоды, но в проблемах производительности, чувствительных ко времени, у вас в симуляторах пара удачно расположенных изменений, таких как эта, может быть разницей между достижением вашей цели синхронизации и нет. Я думаю, что могу с одной стороны рассчитывать, сколько раз окупилось за 20 лет работы над программным обеспечением, но это не ноль.
Берин Лорич
@BerinLoritsch Опять же, в целом я согласен с вами, но в данном конкретном случае я не согласен. Я предоставил свой собственный ответ, но лично я не видел никаких инструментов, которые бы помечали или даже давали вам способы потенциально идентифицировать проблемы производительности, связанные с размером памяти в стеке функции.
Эрик
@ Док, я исправил это. Что касается второго вопроса, я почти согласен с вами.
Берин Лорич
45

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

Однако я бы рекомендовал использовать метод 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
Я бы убрал его еще дальше и пошел бы с «return sum> -1000 && sum <1000;».
17 из 26
36
@Corey любой порядочный оптимизатор будет использовать регистр процессора для sumпеременной, что приведет к нулевому использованию памяти. И даже если нет, это всего лишь одно слово памяти в «листовом» методе. Учитывая, насколько невероятно бесполезной может быть потеря Java или C # из-за их ГХ и объектной модели, локальная intпеременная буквально не использует заметную память. Это бессмысленная микрооптимизация.
Амон
10
@Corey: если он « немного сложнее», он, вероятно, не станет «заметным использованием памяти». Может быть, если вы создадите действительно более сложный пример, но это делает его другим вопросом. Также обратите внимание, что только из-за того, что вы не создаете определенную переменную для выражения, для сложных промежуточных результатов среда выполнения может по-прежнему внутренне создавать временные объекты, поэтому она полностью зависит от деталей языка, среды, уровня оптимизации и все, что вы называете "заметным".
Док Браун
8
В дополнение к перечисленным выше пунктам, я уверен , как C # / Java выбирает для хранения sumбы деталь реализации , и я сомневаюсь , что кто -то мог бы сделать убедительные доводы в отношении того или нет глупый трюк , как избежать один местный intприведет к тому или это количество использования памяти в долгосрочной перспективе. ИМО читабельность важнее. Читаемость может быть субъективной, но, FWIW, лично я бы предпочел, чтобы вы никогда не делали одни и те же вычисления дважды, не для использования процессора, а потому, что мне нужно проверять ваше добавление только один раз, когда я ищу ошибку.
июля
2
... также обратите внимание, что языки для сборки мусора в целом являются непредсказуемым, «взбалтывающимся морем памяти», которое (для C # в любом случае) может быть очищено только при необходимости , я помню, как создавала программу, которая выделяла гигабайты оперативной памяти, и она только запускалась » убираться за собой, когда памяти стало мало. Если GC не нужно запускать, это может занять приятное время и сэкономить ваш процессор для более неотложных дел.
июля
35

Вы можете сделать лучше, чем оба

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

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

Интервьюер, как и другие ответы, говорит, что это растительная жизнь, и он не имеет никакого дела, проводящего техническое интервью.

Тем не менее, его вопрос действителен. И ответ на вопрос, когда вы оптимизируете и как, когда вы доказали, что это необходимо, и вы профилировали его, чтобы точно доказать, какие части нуждаются в этом . Известно, что Кнут говорит, что преждевременная оптимизация - это корень всего зла, потому что слишком легко попытаться найти ненужные разделы или внести изменения (как, например, ваш интервьюер), которые не имеют никакого эффекта, и при этом упустить места, которые действительно в этом нуждаются. Пока у вас нет веских доказательств, что это действительно необходимо, ясность кода является более важной целью.

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

Грэхем
источник
2
@ Кори, я вполне уверен, что Грэхем прикрепил запрос «он бросил мне вызов решить ту же проблему с меньшим количеством переменных», как и ожидалось. Если я был бы интервьюер, я бы ожидать , что ответ, не двигаясь a+bв ifи делать это дважды. Вы понимаете это неправильно: «Он был доволен и сказал, что это уменьшит использование памяти этим методом», - он был добр к вам, скрывая свое разочарование этим бессмысленным объяснением памяти. Вы не должны относиться к этому серьезно, чтобы задать вопрос здесь. Вы получили работу? Я думаю, вы этого не сделали :-(
Синатр
1
Вы применяете 2 преобразования одновременно: вы превратили 2 условия в 1, используя abs(), и у вас также есть одно return, вместо одного, когда условие истинно ("if ветвь"), и еще одно, когда оно ложно ( "еще ветка"). Когда вы меняете код таким образом, будьте осторожны: есть риск непреднамеренного написания функции, которая возвращает true, когда она должна возвращать false, и наоборот. Что именно здесь и произошло. Я знаю, что вы сосредоточены на другом, и вы хорошо поработали над этим. Тем не
Фабио Турати
2
@FabioTurati Хорошо заметили - спасибо! Я обновлю ответ. И это хороший момент в отношении рефакторинга и оптимизации, что делает цитату Кнута еще более актуальной. Мы должны доказать, что нам нужна оптимизация, прежде чем идти на риск.
Грэм
2
Большинство процессоров (и, следовательно, компиляторов) могут выполнять abs () за одну операцию. К сожалению, не относится к целым числам. ARM64 имеет условное отрицание, которое он может использовать, если флаги уже установлены из adds, и ARM имеет предикат reverse-sub ( rsblt= reverse-sub, если меньше), но все остальное требует нескольких дополнительных инструкций для реализации abs(a+b)или abs(a). godbolt.org/z/Ok_Con показывает выходные данные asm для 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это хитрый трюк, но я должен упомянуть (на случай, если это не подразумевается из остальной части ответа), из моих методов опыта, которые связываются с параметрами, может быть очень трудно отлаживать и поддерживать.
июля
1
Я согласен @jrh. Я - решительный сторонник РПЦ, и подобные вещи совсем не похожи.
Джон Ву
3
«Оборудование дешевое, программисты дорогие». В мире бытовой электроники это утверждение неверно. Если вы продаете миллионы устройств, то это очень хорошая инвестиция - потратить 500 000 долларов на дополнительные расходы на разработку, чтобы сэкономить 0,10 доллара на стоимости оборудования на единицу.
Барт ван Инген Шенау
2
@JohnWu: Вы упростили ifпроверку, но забыли отменить результат сравнения; Ваша функция теперь возвращается, trueкогда неa + b находится в диапазоне. Либо добавьте a к внешнему виду условия ( ), либо распределите инвертирующие тесты, чтобы получить Или для !return !(a > 1000 || a < -1000)!return a <= 1000 && a >= -1000;return -1000 <= a && a <= 1000;
корректной
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 МБ ОЗУ, было необходимо снизить производительность - как и в случае проблемы 2000 года, получить несколько сотен тысяч записей, каждая из которых имеет несколько байт памяти, которые теряются из-за неиспользуемых переменных и ссылки и т.д., и это быстро складывается, когда у вас есть только 256 КБ ОЗУ. Теперь, когда мы имеем дело с машинами, имеющими несколько гигабайт оперативной памяти, экономия даже нескольких МБ ОЗУ в сравнении с удобочитаемостью и удобством сопровождения кода не является хорошей сделкой.
июня
@ivanivan: я не думаю, что проблема y2k была действительно о памяти. С точки зрения ввода данных, ввод двух цифр более эффективен, чем ввод четырех, а хранить введенные данные проще, чем преобразовывать их в какую-либо другую форму.
суперкат
10
Теперь вам нужно проследить через 2 функции, чтобы увидеть, что происходит. Вы не можете принять это за чистую монету, потому что вы не можете сказать по названию, являются ли они инклюзивными или эксклюзивными пределами. И если вы добавите эту информацию, имя функции будет длиннее, чем код для ее выражения.
Питер
1
Оптимизируйте удобочитаемость и выполняйте небольшие простые функции - конечно, согласитесь. Но я категорически не согласен , что переименовывать aи bк number1и number2пособия читаемость в любом случае. Кроме того, ваше именование функций противоречиво: зачем IsSumInRangeжестко кодировать диапазон, если IsValueInRangeон принимает его в качестве аргументов?
оставлено около
1-я функция может переполниться. (Как и код других ответов.) Хотя сложность кода, защищенного от переполнения, является аргументом для помещения его в функцию.
Philipxy
6

Короче говоря, я не думаю, что этот вопрос имеет большое значение в современных вычислениях, но с исторической точки зрения это интересное упражнение.

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

В нынешнюю эпоху, я думаю, вам будет трудно найти систему, в которой наличие или отсутствие локальной переменной в методе будет иметь заметное значение. Чтобы значение переменной имело значение, метод должен быть рекурсивным с ожидаемой глубокой рекурсией. Даже тогда вполне вероятно, что глубина стека будет превышена, вызывая исключения переполнения стека до того, как сама переменная вызовет проблему. Единственный реальный сценарий, в котором это может быть проблемой, - это очень большие массивы, выделенные в стеке рекурсивным методом. Но это также маловероятно, так как я думаю, что большинство разработчиков дважды подумают о ненужных копиях больших массивов.

Эрик
источник
4

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

Но оптимизация этой функции - полная чушь. Если бы вы могли сэкономить место, было бы, возможно, 8 байт во время работы функции (которая восстанавливается, когда функция возвращается), так что это абсолютно бессмысленно. Если бы вы могли сэкономить время, это были бы отдельные числа наносекунд. Оптимизация этого - пустая трата времени.

gnasher729
источник
3

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

Тогда это зависит от того, что вам нужно больше всего для вашего приложения. Скорость обработки? Время отклика? След памяти? Ремонтопригодность? Согласованность в дизайне? Все зависит от вас.

Мартин Маат
источник
4
Пометка: по крайней мере .NET (язык сообщения не указан) не дает никаких гарантий относительно того, что локальные переменные размещаются «в стеке». Посмотрите, "Стек - это деталь реализации" Эрика Липперта.
июля
1
@jrh Локальные переменные в стеке или куче могут быть деталями реализации, но если кто-то действительно хочет переменную в стеке, есть stackallocи сейчас Span<T>. Возможно полезно в горячей точке, после профилирования. Кроме того, некоторые из документов вокруг структур подразумевают, что типы значений могут быть в стеке, в то время как ссылочные типы не будут. Во всяком случае, в лучшем случае вы могли бы избежать немного GC.
Боб
2

Как уже говорили другие ответы, вам нужно подумать, для чего вы оптимизируете.

В этом примере я подозреваю, что любой приличный компилятор сгенерирует эквивалентный код для обоих методов, поэтому решение не повлияет на время выполнения или память!

Что же влияет на это читаемость кода. (Код предназначен для чтения людьми, а не только компьютерами.) Между этими двумя примерами нет большой разницы; когда все остальные вещи равны, я считаю краткость добродетелью, поэтому я, вероятно, выберу метод B. Но все остальные вещи редко бывают равными, и в более сложном случае в реальном мире это может иметь большой эффект.

Что нужно учитывать:

  • Есть ли у промежуточного выражения побочные эффекты? Если он вызывает какие-либо нечистые функции или обновляет какие-либо переменные, то, конечно, дублирование будет вопросом правильности, а не стиля.
  • Насколько сложным является промежуточное выражение? Если он выполняет много вычислений и / или вызывает функции, компилятор может не оптимизировать его, что может повлиять на производительность. (Хотя, как сказал Кнут , «мы должны забыть о малой эффективности, скажем, в 97% случаев».)
  • Имеет ли промежуточная переменная какое-либо значение ? Можно ли дать имя, которое поможет объяснить, что происходит? Короткое, но информативное имя может объяснить код лучше, а бессмысленное - просто визуальный шум.
  • Как долго это промежуточное выражение? Если он длинный, то его дублирование может сделать код длиннее и сложнее для чтения (особенно, если оно вызывает разрыв строки); если нет, дублирование может быть короче всего.
gidds
источник
1

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

Но давайте еще раз посмотрим на этот вопрос: это вопрос интервью. Итак, реальная проблема в том, как вы должны ответить на это, если вы хотите попытаться получить работу?

Давайте также предположим, что интервьюер знает, о чем он говорит, и он просто пытается увидеть то, что вы знаете.

Я хотел бы отметить, что, игнорируя оптимизатор, первый может создать временную переменную в стеке, тогда как второй не будет, но будет выполнять вычисления дважды. Поэтому первый использует больше памяти, но работает быстрее.

Вы могли бы упомянуть, что в любом случае для вычисления результата может потребоваться временная переменная, чтобы сохранить результат (чтобы его можно было сравнивать), поэтому независимо от того, назовете ли вы эту переменную или нет, разница не будет.

Затем я бы упомянул, что в действительности код будет оптимизирован и, скорее всего, будет создан эквивалентный машинный код, поскольку все переменные являются локальными. Тем не менее, это зависит от того, какой компилятор вы используете (не так давно я смог получить полезное улучшение производительности, объявив локальную переменную как "final" в Java).

Вы могли бы упомянуть, что стек в любом случае живет на своей собственной странице памяти, поэтому, если ваша дополнительная переменная не вызвала переполнение стека страницей, в действительности она не будет выделять больше памяти. Если это действительно переполняет, это хотело бы полностью новую страницу все же.

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

Все это показывает, что вы знаете, о чем говорите.

Я бы оставил это до конца, чтобы сказать, что было бы лучше сосредоточиться на читабельности вместо этого. Хотя в данном случае это правда, в контексте интервью это можно интерпретировать как «я не знаю о производительности, но мой код читается как история Джанет и Джона ».

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

Производительность кода является серьезной проблемой во многих организациях, и многим организациям нужны программисты, которые понимают это.

В частности, в таких организациях, как Amazon, часть кода имеет огромное влияние. Фрагмент кода может быть развернут на тысячах серверов или миллионах устройств и может вызываться миллиарды раз в день каждый день в году. Там могут быть тысячи подобных фрагментов. Разница между плохим и хорошим алгоритмом может легко быть в тысячу раз больше. Сделайте числа и умножьте все это: это имеет значение. Потенциальные затраты на организацию неработающего кода могут быть очень значительными или даже фатальными, если система исчерпает свои ресурсы.

Кроме того, многие из этих организаций работают в конкурентной среде. Таким образом, вы не можете просто сказать своим клиентам, чтобы они покупали больший компьютер, если программное обеспечение вашего конкурента уже работает нормально на оборудовании, которое у них есть, или если программное обеспечение работает на мобильном телефоне, и его невозможно обновить. Некоторые приложения особенно критичны по производительности (на ум приходят игры и мобильные приложения) и могут жить или умирать в зависимости от их скорости отклика или скорости.

Лично я более двух десятилетий работал над многими проектами, в которых системы выходили из строя или не могли работать из-за проблем с производительностью, и меня вызвали для оптимизации этих систем, и во всех случаях это было из-за плохого кода, написанного программистами, которые не понимали влияние того, что они писали. Более того, это никогда не один кусок кода, это всегда везде. Когда я появляюсь, уже поздно думать о производительности: ущерб уже нанесен.

Понимание производительности кода - это хороший навык, который нужно иметь так же, как понимание правильности кода и стиля кода. Это выходит из практики. Сбои производительности могут быть такими же плохими, как и функциональные сбои. Если система не работает, она не работает. Неважно, почему. Точно так же производительность и функции, которые никогда не используются, являются плохими.

Поэтому, если интервьюер спрашивает вас об эффективности, я бы порекомендовал попробовать и продемонстрировать как можно больше знаний. Если вопрос кажется плохим, вежливо укажите, почему вы считаете, что в этом случае это не будет проблемой. Не цитируйте Кнута.

rghome
источник
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 #) вы можете использовать проверенный раздел {}, который может вызвать исключение при переполнении. Да, это сложнее и сложнее, но иногда это то, что нужно.

TomEberhard
источник
Методы были только примерами. Они были написаны не для того, чтобы быть правильными, но чтобы проиллюстрировать актуальный вопрос. Спасибо за вклад, хотя!
Кори П
Я думаю, что вопрос интервью направлен на производительность, поэтому вы должны ответить на вопрос цели. Интервьюер не спрашивает о поведении на пределе. Но интересная сторона в любом случае.
rghome
1
@Corey Хорошие интервьюеры в качестве вопросов к 1) оценить способность кандидата в отношении проблемы, как предложено здесь, но также и 2) как открытие для более широких вопросов (таких как невысказанная функциональная корректность) и глубины связанных знаний - это больше так в последующих карьерных собеседованиях - удачи.
Чукс
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 записей), добавьте еще несколько сотен циклов. Если ваш шаблон случайного доступа действительно вызывает ошибку страницы, в лучшем случае это будет стоить вам несколько десятков тысяч циклов, а в худшем - несколько миллионов.
Теперь подумайте, что вы хотите избежать срочно?

Damon
источник
0

Когда оптимизировать память по сравнению с быстродействием метода?

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


В качестве вопроса об оптимизации кода код вызывает обычную дискуссию, но не достигает цели более высокого уровня: является ли код функционально правильным?

И C ++, и C, и другие считают intпереполнение проблемой из a + b. Это не очень хорошо определено, и C называет это неопределенным поведением . Не указывается «обтекание» - хотя это обычное поведение.

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.

chux
источник
0

О каких компиляторах идет речь, и что за «память»? Потому что в вашем примере, при условии разумного оптимизатора, выражение a+bдолжно обычно храниться в регистре (форме памяти) перед выполнением такой арифметики.

Так что, если мы говорим о тупом компиляторе, который встречается a+bдважды, он выделит больше регистров (памяти) во втором примере, потому что ваш первый пример может просто сохранить это выражение один раз в одном регистре, сопоставленном с локальной переменной, но мы Сейчас мы говорим об очень глупых компиляторах ... если только вы не работаете с другим типом глупых компиляторов, стек которых разливает каждую переменную повсеместно, и в этом случае, возможно, первая из них приведет к большему сожалению для оптимизации, чем секунда*.

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

Все это чрезвычайно спекулятивно, довольно глупо, без измерений / разборки и даже в худшем случае, это не случай «память против производительности» (потому что даже среди худших оптимизаторов, о которых я могу думать, мы не говорим о чем угодно, кроме временной памяти, такой как стек / регистр), это в лучшем случае чисто «производительность», и среди любого разумного оптимизатора два эквивалентны, и если один не использует разумный оптимизатор, почему так мало внимания уделяется оптимизации, которая носит микроскопический характер и особенно отсутствуют измерения? Это похоже на фокус на уровне ассемблера выбора инструкций / выделения регистров, который я бы никогда не ожидал, что кто-то, желающий оставаться продуктивным, будет иметь, например, использование интерпретатора, стек которого проливает все.

Когда оптимизировать память по сравнению с быстродействием метода?

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

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

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

Энергия Дракона
источник