Должен ли я хранить значения массива C в локальных переменных, если я собираюсь ссылаться на них повторно? - PullRequest
3 голосов
/ 19 марта 2009

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

for (i=0; i < a_len; i++) {
    if (a[i] == 0) {
        a[i] = f1(a[i]);
    } else if (a[i] % 2 == 0) {
        a[i] = f2(a[i]);
    } else {
        a[i] = 0;
}

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

Если бы я писал код выше на ассемблере, я бы загружал [i] в ​​регистр один раз, а затем просто использовал это значение каждый раз, потому что я знаю, что [] является частной памятью и не будет меняться между ссылками. Однако даже умный компилятор может выполнять загрузку каждый раз, потому что не может быть уверен, что память не изменилась. (Или я должен явно объявить "a" volatile для компилятора, чтобы не выполнять эту оптимизацию?).

Итак, мой вопрос: стоит ли ожидать большей производительности, переписав с локальной переменной, например:

for (i=0; i < a_len; i++) {
    val = a[i];
    if (val == 0) {
        a[i] = f1(val);
    } else if (val % 2 == 0) {
        a[i] = f2(val);
    } else {
        a[i] = 0;
}

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

Ответы [ 9 ]

14 голосов
/ 19 марта 2009

Очевидный ответ, конечно, сначала написать его наиболее читаемым / понятным / понятным способом, затем скомпилировать его с максимально возможной оптимизацией, а затем выполнить сравнительный анализ и профилировать это.

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

7 голосов
/ 19 марта 2009

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

for (i=0; i < a_len; i++) {

    int val = a[i];  /* or whatever type */
    int result = 0;  /* default result */

    if (val == 0) {
        result = f1(val);
    } else if (val % 2 == 0) {
        result = f2(val);
    } 

    a[i] = result;
}

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

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

for (p=&a[0]; p < &a[a_len]; ++p) {

    int val = *p;    /* or whatever type */
    int result = 0;  /* default result */

    if (val == 0) {
        result = f1(val);
    } else if (val % 2 == 0) {
        result = f2(val);
    } 

    *p = result;
}

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

То, будет ли компилятор многократно перезагружаться из чего-то вроде [i] или нет, зависит от потока управления и от того, является ли объект, к которому осуществляется доступ, глобальным или его адрес был взят и передан другому объекту.

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

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


Интересно, почему уценка убирает пустые строки из блоков в формате кода?

4 голосов
/ 19 марта 2009

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

3 голосов
/ 19 марта 2009

Функции f1 и f2, похоже, имеют одинаковую подпись. Как по-другому они себя ведут? Вам действительно нужен чек на улице? Или вы можете встроить логику в одну функцию?

Если у вас есть if-else лестница вместо двух таких функций, попробуйте вместо этого использовать массив указателей на функции. Используйте значение a[ i ], чтобы проиндексировать этот массив и вызвать правильную функцию.

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

Что касается вашего вопроса: да, большинство компиляторов, вероятно, оптимизируют считывание памяти, если a[ i ] не объявлено volatile.

1 голос
/ 20 марта 2009

ДиркГентли ответ:

Да, большинство компиляторов оптимизировать чтение памяти, если [ я]

Иногда компилятор не оптимизирует код, когда он имеет дело с указателем, который «может иметь псевдоним». В вашем случае, Ник, если вы задаете «a» в качестве параметра функции, function (int * a), то компилятор может предположить, что указатель на «a» является псевдонимом и, следовательно, не оптимизируется.

Если вы указали качество указателя как «int * restrict a», то компилятор узнает, что «a» не является псевдонимом, и оптимизирует его.

Единственный способ узнать на 100%, оптимизирует ли компилятор, - проверить сборку !

0 голосов
/ 19 марта 2009

Вы говорите: «Если бы я писал код выше на ассемблере ...», поэтому я предполагаю, что вы знаете язык ассемблера.

Мой совет: посмотрите выходные данные компилятора на критические участки кода, посмотрите, что на самом деле происходит.

0 голосов
/ 19 марта 2009

Подсказка по оптимизации

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

int* var;//Int or whatever type a[] is
for (i=0; i < a_len; i++) {
    val = &a[i];
    if (*val == 0) {
        f1(val);//// Set the valur inside f1
    } else if (*val % 2 == 0) {
        f2(val);// Set the valur inside f2
    } else {
        *val = 0;
}

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

#define is_divisible(dividend, divisor) ((((dividend)/(divisor)) * (divisor))==(dividend))

использование:

else if (is_divisible(val,2)) {

Это быстрее, по крайней мере, в большинстве случаев, которые я тестировал.

Редактировать: верно, что прибыль не так велика при использовании вычислений по модулю, использующих только «% 2». Но если вы когда-либо зависаете при большем значении, чем 2, чтобы выполнить операцию по модулю и просто заинтересованы в том, чтобы по модулю возвращался ноль, мой макрос работает быстрее во всех компиляторах, которые я использовал

0 голосов
/ 19 марта 2009
  1. Не оптимизируйте, если вы не знаете,
  2. Ваш компилятор, вероятно, поступит правильно
  3. Мне легче читать последнюю версию
  4. (маргинальный) легче изолировать последнюю версию от побочных эффектов в многопоточной программе
0 голосов
/ 19 марта 2009

Массив в C по сути является указателем.

локальные переменные дешевы.

Я считаю, что первый пример немного проще для чтения, потому что я не спрашиваю, для чего нужен "val". если бы «val» и «a» имели лучшие имена, я бы рискнул сказать, что второй пример улучшит читабельность.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...