В C ++ мне нужно кэшировать переменные или позволить компилятору сделать оптимизацию? (Сглаживание)

114

Рассмотрим следующий код ( pимеет тип unsigned char*и bitmap->widthимеет некоторый целочисленный тип, который точно неизвестен и зависит от того, какую версию какой-либо внешней библиотеки мы используем):

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

Стоит ли его оптимизировать [..]

Может ли быть случай, когда это может дать более эффективные результаты, написав:

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

... или компилятор может оптимизировать это тривиально?

Какой код вы считаете «лучшим»?

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

Ярон Коэн-Тал
источник
19
Если *pимеет тот же тип, что и widthтогда, его нетривиально оптимизировать, поскольку он pможет указывать widthи изменять его внутри цикла.
emlai
31
Спрашивать, оптимизирует ли компилятор ту или иную операцию, обычно неверно. Что вас (обычно) в конечном итоге интересует, так это то, какая версия работает быстрее, и вам нужно просто измерить.
SirGuy
4
@GuyGreer Я согласен, хотя я бы сказал, что вопрос хороший или, по крайней мере, интересный, но, к сожалению, ответ таков: «Вы должны измерить его для каждого варианта использования». Причина в том, что функциональность переносима, а производительность - нет. Так что на самом деле это зависит от каждой части процесса сборки, начиная с компилятора и заканчивая целевым сайтом (комбинация ОС / оборудования). И, конечно, лучше всего предположить, что компилятор в этом умнее человека.
luk32
19
Если бы я был компилятором, я бы увидел, что ваши два примера не совпадают. Возможно, это pуказывает на ту же память, что и bitmap->width. Поэтому я не могу юридически оптимизировать первый пример до второго.
Mysticial
4
Где хранится "p"? Я бы предположил, что вы можете получить действительно огромный выигрыш в производительности, выполнив что-то вроде "char * restrict p2 = p;" а затем используйте «p2» вместо «p» в вашем цикле. Затем, если вы хотите, чтобы изменения в «p2» применялись обратно к p, используйте «p + = (p2-p);». Обратите внимание, что ни один указатель, записанный в течение времени жизни p2 указателем, не скопированным из формы p2, не может быть прочитан с использованием указателя, скопированного из p2, и наоборот, и никакая копия p2 не может использоваться для каких-либо целей после времени жизни p2, но компилятор может использовать их факты, позволяющие проводить оптимизацию, которую нельзя выполнить никакими другими средствами.
supercat

Ответы:

81

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

Источник unoptimized.cpp

примечание: этот код не предназначен для выполнения.

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    for (unsigned x = 0 ; x < static_cast<unsigned>(bitmap.width) ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

Источник optimized.cpp

примечание: этот код не предназначен для выполнения.

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    const unsigned width = static_cast<unsigned>(bitmap.width);
    for (unsigned x = 0 ; x < width ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

компиляция

  • $ g++ -s -O3 unoptimized.cpp
  • $ g++ -s -O3 optimized.cpp

Сборка (unoptimized.s)

    .file   "unoptimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    mov %eax, %edx
    addl    $1, %eax
    movq    (%rsi,%rdx,8), %rdx
    movb    $0, (%rdx)
    cmpl    bitmap(%rip), %eax
    jb  .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

Сборка (optimized.s)

    .file   "optimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    subl    $1, %eax
    leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    movq    (%rsi,%rax), %rdx
    addq    $8, %rax
    cmpq    %rcx, %rax
    movb    $0, (%rdx)
    jne .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

разница

$ diff -uN unoptimized.s optimized.s
--- unoptimized.s   2015-11-24 16:11:55.837922223 +0000
+++ optimized.s 2015-11-24 16:12:02.628922941 +0000
@@ -1,4 +1,4 @@
-   .file   "unoptimized.cpp"
+   .file   "optimized.cpp"
    .text
    .p2align 4,,15
 .globl main
@@ -10,16 +10,17 @@
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
+   subl    $1, %eax
+   leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
 .L3:
-   mov %eax, %edx
-   addl    $1, %eax
-   movq    (%rsi,%rdx,8), %rdx
+   movq    (%rsi,%rax), %rdx
+   addq    $8, %rax
+   cmpq    %rcx, %rax
    movb    $0, (%rdx)
-   cmpl    bitmap(%rip), %eax
-   jb  .L3
+   jne .L3
 .L2:
    xorl    %eax, %eax
    ret

Сгенерированная сборка для оптимизированной версии действительно загружает ( lea) widthконстанту, в отличие от неоптимизированной версии, которая вычисляет widthсмещение на каждой итерации ( movq).

Когда у меня будет время, я в конце концов опубликую тест по этому поводу. Хороший вопрос.

МКЦ
источник
3
Было бы интересно посмотреть, создавался ли код по-другому, если вы выполняете приведение к const unsignedвместо unsignedнеоптимизированного случая.
Марк Рэнсом,
2
@MarkRansom Я думаю, это не должно иметь значения: "обещание" быть константным только во время одного сравнения, а не для всего цикла,
Хаген фон Эйтцен
13
Пожалуйста , никогда не используйте функцию mainдля тестирования для оптимизации. Gcc намеренно отмечает его как холодный и, таким образом, отключает для него некоторые оптимизации. Я не знаю, так ли это здесь, но это важная привычка.
Marc Glisse
3
@MarcGlisse Вы правы на 100%. Написал на скорую руку, поправлю.
YSC
3
Вот ссылка на обе функции в одном модуле компиляции на Godbolt , если bitmapэто глобальный. Версия без CSEd использует операнд памяти для cmp, что не является проблемой для perf в этом случае. Если бы он был локальным, компилятор мог бы предположить, что другие указатели не могут «знать о нем», и указывать на него. Неплохо хранить выражения, включающие глобальные переменные, во временных переменных, если это улучшает (или не ухудшает) читаемость или если производительность критична. Если ничего не происходит, такие местные жители обычно живут в регистрах, и их никогда не проливают.
Питер Кордес
38

На самом деле в вашем фрагменте кода недостаточно информации, чтобы сказать, и единственная вещь, о которой я могу думать, - это алиасинг. С нашей точки зрения, это довольно очевидно , что вы не хотите , pи bitmapк точке в том же месте в памяти, но компилятор не знает , что и (потому что pэто типа char*), компилятор должен сделать этот код работать , даже если pи bitmapперекрытие.

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

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

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

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

SirGuy
источник
1
@GuyGreer: это главный блокировщик оптимизации; Я считаю прискорбным, что правила языка сосредоточены на правилах, касающихся эффективных типов, а не на определении ситуаций, когда запись и чтение различных элементов являются или не являются непоследовательными. Правила, написанные в таком термине, могли бы гораздо лучше удовлетворить потребности компилятора и программиста, чем существующие.
supercat
3
@GuyGreer - разве restrictквалификатор не был бы ответом на проблему сглаживания в этом случае?
LThode
4
По моему опыту, restrictв основном это случайные ошибки. MSVC - единственный компилятор, который я видел, который, кажется, делает это правильно. ICC теряет информацию о псевдонимах через вызовы функций, даже если они встроены. И GCC обычно не может получить никаких преимуществ, если вы не объявите каждый входной параметр как restrict(в том числе thisдля функций-членов).
Mysticial
1
@Mystical: нужно помнить, что charпсевдонимы всех типов, поэтому, если у вас есть char *, вы должны использовать его restrictдля всего. Или, если вы отключили строгие правила псевдонима GCC, -fno-strict-aliasingтогда все будет считаться возможным псевдонимом.
Zan Lynx
1
@Ray Самым последним предложением restrict-подобной семантики в C ++ является N4150 .
TC
24

Хорошо, ребята, я измерил GCC -O3(используя GCC 4.9 в Linux x64).

Оказывается, вторая версия работает на 54% быстрее!

Так что, думаю, дело в алиасинге, я об этом не думал.

[Редактировать]

Я снова попробовал первую версию со всеми указателями, определенными с помощью __restrict__, и результаты такие же. Странно ... Либо сглаживание не проблема, либо по какой-то причине компилятор плохо его оптимизирует даже с __restrict__.

[Изменить 2]

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

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

И измерил (пришлось использовать "-mcmodel = large", чтобы связать его). Потом попробовал:

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

Результаты измерений были такими же - похоже, компилятор смог оптимизировать его самостоятельно.

Затем я попробовал исходные коды (с указателем p), на этот раз pтипа std::uint16_t*. И снова результат был таким же - за счет строгого алиасинга. Затем я попытался построить с помощью «-fno-strict-aliasing» и снова увидел разницу во времени.

Ярон Коэн-Тал
источник
4
Кажется, это должен быть комментарий, хотя технически он отвечает на вопрос. Также обратите внимание, к сожалению, вы не продемонстрировали, что такое алиасинг было. Это кажется вероятным, определенно правдоподобным, но это другое, чем заключить, что это было именно так.
SirGuy
@GuyGreer: См. Мой [edit 2] - теперь я думаю, что это в значительной степени доказано.
Ярон Коэн-Тал
2
Мне просто интересно, почему вы начали использовать переменную «i», когда в вашем цикле есть «x»?
Jesper Madsen
1
Неужели только мне кажется, что фразу на 54% труднее понять? Вы имеете в виду, что это в 1,54 раза быстрее неоптимизированного, или что-то еще?
Родди
3
@ YaronCohen-Tal в два раза быстрее? Впечатляет, но не то, что я бы понял под словом «на 54% быстрее»!
Родди,
24

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

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

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

for (unsigned x = static_cast<unsigned>(bitmap->width);x > 0;  x--)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

Обратите внимание, что эта версия цикла дает значения x в диапазоне 1..width, а не в диапазоне 0 .. (width-1). В вашем случае это не имеет значения, потому что вы на самом деле ни для чего не используете x, но об этом нужно знать. Если вам нужен цикл обратного отсчета со значениями x в диапазоне 0 .. (ширина-1), вы можете это сделать.

for (unsigned x = static_cast<unsigned>(bitmap->width); x-- > 0;)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

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

plugwash
источник
2
Я видел, что второй регистр отформатирован как x --> 0, что приводит к оператору «даунто». Очень забавно. PS Я не считаю, что создание переменной для конечного условия отрицательно для читабельности, на самом деле может быть наоборот.
Марк Рэнсом
Это действительно зависит от ситуации, иногда оператор становится настолько ужасным, что разбиение его на несколько операторов улучшает читаемость, но я не верю, что это так.
plugwash
1
+1 Хорошее наблюдение, хотя я бы сказал, что подъем static_cast<unsigned>(bitmap->width)и использование widthвместо этого в цикле на самом деле улучшает удобочитаемость, потому что теперь читателю становится меньше вещей, которые нужно анализировать на каждую строку. Однако мнения других могут отличаться.
SirGuy
1
Есть много других ситуаций, когда обратный отсчет лучше (например, при удалении элементов из списка). Не знаю, почему это не делается чаще.
Ян Голдби
3
Если вы хотите писать циклы, которые больше похожи на оптимальный asm, используйте do { } while(), потому что в ASM вы создаете циклы с условной ветвью в конце. Обычная for(){}и while(){}петля требует дополнительных инструкций , чтобы проверить условие цикла один раз перед циклом, если компилятор не может доказать это всегда работает по крайней мере один раз. В любом случае используйте for()или, while()когда полезно проверить, должен ли цикл запускаться один раз, или когда он более читабелен.
Питер Кордес
11

Единственное, что может помешать оптимизации, - это строгое правило сглаживания . Вкратце :

«Строгий псевдоним - это сделанное компилятором C (или C ++) допущение, что разыменование указателей на объекты разных типов никогда не будет ссылаться на одну и ту же ячейку памяти (т.е. псевдонимы друг друга)».

[...]

Исключением из правила является a char*, которому разрешено указывать на любой тип.

Исключение относится также к unsignedи signed charуказатели.

Так обстоит дело с вашим кодом: вы изменяете, *pчерез pкакой является unsigned char*, поэтому компилятор должен предположить, что он может указывать на bitmap->width. Следовательно, кеширование bitmap->widthявляется недопустимой оптимизацией. Это поведение предотвращения оптимизации показано в ответе YSC .

Если и только если бы он pуказывал на не- тип charи не- decltype(bitmap->width)тип, кеширование могло бы быть возможной оптимизацией.

emlai
источник
10

Первоначально заданный вопрос:

Стоит ли его оптимизировать?

И мой ответ на этот вопрос (я получил хорошее сочетание голосов как за, так и против ...)

Пусть об этом позаботится компилятор.

Компилятор почти наверняка справится лучше, чем вы. И нет никакой гарантии, что ваша «оптимизация» лучше, чем «очевидный» код - вы ее измерили ??

Что еще более важно, есть ли у вас доказательства того, что оптимизируемый код каким-либо образом влияет на производительность вашей программы?

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

Конечно, совсем другой вопрос:

Как узнать, стоит ли оптимизировать фрагмент кода?

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

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

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

Что вы вообще имеете в виду под «оптимизацией»?

Если вы не пишете «оптимизированный» код, ваш код должен быть настолько ясным, чистым и кратким, насколько вы можете его сделать. Аргумент «Преждевременная оптимизация - зло» не является оправданием небрежного или неэффективного кода.

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

Но часто с «медленным» кодом эти микрооптимизации становятся последним средством. В первую очередь следует изучить алгоритмы и структуры данных. Есть ли способ вообще избежать работы? Можно ли заменить линейный поиск двоичным? Будет ли связанный список здесь быстрее, чем вектор? Или хеш-таблица? Могу ли я кэшировать результаты? Принятие здесь хороших «эффективных» решений часто может повлиять на производительность на порядок или больше!

Родди
источник
12
Когда вы выполняете итерацию по ширине растрового изображения, логика цикла может составлять значительную часть времени, затрачиваемого на цикл. Вместо того чтобы беспокоиться о преждевременной оптимизации, в этом случае лучше разработать передовые методы, которые будут эффективны с самого начала.
Марк Рэнсом
4
@MarkRansom частично согласился: но «передовой практикой» будет либо: использовать существующую библиотеку или вызов API для заполнения изображений, либо b: заставить графический процессор сделать это за вас. OP никогда не должна быть такой неизмеримой микрооптимизации. И откуда вы знаете, что этот код выполняется более одного раза или с растровыми изображениями шириной более 16 пикселей ...?
Родди
@Veedrac. Оцените обоснование -1. С тех пор, как я ответил, суть вопроса слегка изменилась. Если вы думаете, что (развернутый) ответ по-прежнему бесполезен, пора мне его удалить ... В любом случае «Стоит ли ...» всегда в первую очередь основывается на мнении.
Родди
@Roddy Я ценю правки, они действительно помогают (и мой комментарий, вероятно, в любом случае звучал слишком резко). Однако я все еще в затруднении, поскольку это действительно ответ на вопрос, который не подходит для Stack Overflow. Похоже, что правильный ответ будет конкретным для фрагмента, как и ответы с большим количеством голосов.
Veedrac
6

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

for (unsigned int x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
{
  *p++ = 0xAA;
  *p++ = 0xBB;
  *p++ = 0xCC;
}

Это будет быстрее с менее умным компилятором, отладочной сборкой или некоторыми флагами компиляции.

Edit1 : Размещение постоянной операции вне цикла - хороший шаблон программирования. Показывает понимание основ работы с машиной, особенно на C / C ++. Я бы сказал, что попытки проявить себя должны быть на людях, которые не следуют этой практике. Если компилятор наказывает за хороший шаблон, это ошибка в компиляторе.

Edit2:: Я сравнил свое предложение с исходным кодом на vs2013, получил улучшение% 1. Можем ли мы сделать лучше? Простая ручная оптимизация дает улучшение в 3 раза по сравнению с исходным циклом на машине x64, не прибегая к экзотическим инструкциям. В приведенном ниже коде используется система с прямым порядком байтов и правильно выровненное растровое изображение. ТЕСТ 0 - оригинальный (9 секунд), ТЕСТ 1 - быстрее (3 секунды). Бьюсь об заклад, кто-нибудь может сделать это еще быстрее, и результат теста будет зависеть от размера растрового изображения. Определенно скоро в будущем компилятор сможет производить стабильно самый быстрый код. Боюсь, это будет будущее, когда компилятор будет также программистом ИИ, поэтому мы останемся без работы. Но пока просто напишите код, который показывает, что вы знаете, что дополнительная операция в цикле не требуется.

#include <memory>
#include <time.h>

struct Bitmap_line
{
  int blah;
  unsigned int width;
  Bitmap_line(unsigned int w)
  {
    blah = 0;
    width = w;
  }
};

#define TEST 0 //define 1 for faster test

int main(int argc, char* argv[])
{
  unsigned int size = (4 * 1024 * 1024) / 3 * 3; //makes it divisible by 3
  unsigned char* pointer = (unsigned char*)malloc(size);
  memset(pointer, 0, size);
  std::unique_ptr<Bitmap_line> bitmap(new Bitmap_line(size / 3));
  clock_t told = clock();
#if TEST == 0
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
    //for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#else
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    unsigned x = 0;
    for (const unsigned n = static_cast<unsigned>(bitmap->width) - 4; x < n; x += 4)
    {
      *(int64_t*)p = 0xBBAACCBBAACCBBAALL;
      p += 8;
      *(int32_t*)p = 0xCCBBAACC;
      p += 4;
    }

    for (const unsigned n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#endif
  double ms = 1000.0 * double(clock() - told) / CLOCKS_PER_SEC;
  printf("time %0.3f\n", ms);

  {
    //verify
    unsigned char* p = pointer;
    for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      if ((*p++ != 0xAA) || (*p++ != 0xBB) || (*p++ != 0xCC))
      {
        printf("EEEEEEEEEEEEERRRRORRRR!!!\n");
        abort();
      }
    }
  }

  return 0;
}
0kcats
источник
Вы можете сэкономить еще 25% на 64-битной версии, если используете три int64_t вместо int64_t и int32_t.
Антонин Лейсек
5

Следует учитывать две вещи.

А) Как часто будет выполняться оптимизация?

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

Б) Усложнит ли это исправление / устранение неполадок кода?

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

Тем не менее, я лично использую пример B.

soulsabr
источник
4

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

Гийом Расико
источник
4

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

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

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

Например, в Windows x64 необходимо вызвать специальный вызов API __chkstkв преамбуле функции, если функция будет использовать более 1 страницы локальных переменных. Эта функция дает окнам возможность управлять страницами защиты, которые они используют для расширения стека при необходимости. Если ваша дополнительная переменная увеличивает использование стека с 1 страницы до 1 или выше, ваша функция теперь обязана вызывать __chkstkкаждый раз, когда она вводится. Если бы вы оптимизировали этот цикл на медленном пути, вы могли бы замедлить быстрый путь вниз больше, чем вы сэкономили на медленном пути!

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

Корт Аммон
источник
4
Я бы хотел, чтобы C и C ++ предоставляли больше способов явной идентификации вещей, которые программисту не интересны. Они не только предоставят компиляторам больше шансов на оптимизацию, но и избавят других программистов, читающих код, от необходимости угадывать, может ли он, например, перепроверять растровое изображение-> ширину каждый раз, чтобы убедиться, что изменения в нем влияют на цикл, или может ли он кэшировать растровое изображение-> ширина, чтобы гарантировать, что его изменения не повлияют на цикл. Наличие средства сказать «Кешировать это или нет - мне все равно» проясняет причину выбора программиста.
supercat
@supercat Я полностью согласен, так как можно увидеть, если посмотреть на груды татуированных неудачных языков, которые я пытался написать, чтобы решить эту проблему. Я обнаружил, что чрезвычайно трудно определить, «что» кого-то не волнует, без такого нечестивого синтаксиса, что оно того не стоит. Напрасно продолжаю поиски.
Cort Ammon
Невозможно определить его во всех случаях, но я думаю, что во многих случаях может помочь система типов. Слишком уж C решил сделать символьные типы «универсальным средством доступа» вместо того, чтобы иметь квалификатор типа, который был немного более свободным, чем «изменчивый», который можно было бы применить к любому типу, с семантикой, согласно которой доступ к таким типам будет обрабатываться в последовательности с доступы неквалифицированного эквивалентного типа, а также доступ ко всем типам переменных с тем же квалификатором. Это поможет прояснить, использует ли кто-то типы символов, потому что ему нужен ...
supercat
... псевдонима, или использовали ли вы их, потому что они были подходящего размера, чтобы соответствовать вашим потребностям. Также было бы полезно иметь явные барьеры псевдонима, которые во многих случаях можно было бы разместить за пределами циклов, в отличие от неявных барьеров, связанных с доступом символьного типа.
supercat
1
Это разумный разговор, но, как правило, если вы уже выбрали C для своей задачи, вероятно, производительность очень важна, и должны применяться другие правила. В противном случае может быть лучше использовать Ruby, Java, Python или что-то подобное.
Audrius Meskauskas
4

Сравнение неверно , так как два фрагмента кода

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)

и

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x<width ;  ++x)

не эквивалентны

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

В вашем оптимизированном случае локальной переменнойbitmap->width в какой-то момент во время выполнения программы присваивается значение . Компилятор может проверить, что на самом деле это не изменилось.

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

Компилятор может работать настолько хорошо, насколько позволяет ваш код.

g24l
источник
2

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

Винаяк С.М.
источник
1

Компилятор не может оптимизировать, bitmap->widthпотому что значение widthможет быть изменено между итерациями. Есть несколько наиболее частых причин:

  1. Многопоточность. Компилятор не может предсказать, собирается ли другой поток изменить значение.
  2. Модификация внутри цикла, иногда непросто определить, будет ли изменена переменная внутри цикла.
  3. Это вызов функции, например , iterator::end()или container::size()так что трудно предсказать , если он будет всегда возвращает тот же результат.

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

ST3
источник