Чи потрібно мені в 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. Тому я не можу юридично оптимізувати перший приклад до другого.
Містичний

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

Асамблея (неоптимізовано)

    .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

Збірка (оптимізовано.с)

    .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 Я думаю, це не повинно змінити значення: "обіцянка" бути const є лише під час єдиного порівняння, а не для цілого циклу
Hagen von Eitzen

13
Будь ласка , ніколи не використовуйте функцію mainдля тестування для оптимізації. Gcc цілеспрямовано позначає його як холодний і тим самим відключає деякі оптимізації для нього. Я не знаю, чи так це тут, але це важлива звичка вникати.
Марк Глісс

3
@MarcGlisse Ти маєш право на 100%. Я написав це поспіхом, це вдосконалю.
YSC

3
Ось посилання на обидві функції в одному блоці компіляції на godbolt , якщо припустити, що bitmapце глобальний. Версія без CSEd використовує операнд пам’яті для cmp, що не є проблемою для perf в цьому випадку. Якби це був локальний, компілятор міг би припустити, що інші вказівники не могли «знати про це» і вказати на нього. Непогано зберігати вирази, що включають глобали, у тимчасові змінні, якщо це покращує (або не зашкодить) читабельність, або якщо продуктивність є критичною. Якщо там багато чого не відбувається, такі місцеві жителі зазвичай можуть просто жити в реєстрах, і ніколи їх не розкидати.
Пітер Кордес

38

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

Це означає, що якщо цикл змінюється bitmap->widthчерез покажчик, pто це слід побачити при повторному читанні bitmap->widthпізніше, що, в свою чергу, означає, що зберігання його в локальній змінній було б незаконним.

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

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

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


1
@GuyGreer: це головний оптимізатор-блокатор; Я вважаю прикро, що мовні правила зосереджуються на правилах щодо ефективних типів, а не на виявленні ситуацій, коли записи та читання різних предметів є або не є наслідком. Правила, написані в такому терміні, можуть зробити набагато кращу роботу щодо задоволення потреб компілятора та програміста, ніж нинішні.
supercat

3
@GuyGreer - невже класифікатор не restrictбуде відповіддю на проблему згладжування в цьому випадку?
LThode

4
На мій досвід, restrictце багато в чому хіт-і-міс. MSVC - єдиний компілятор, який я бачив і, здається, робить це належним чином. ICC втрачає інформацію про випробовування через виклики функцій, навіть якщо вони вбудовані. І GCC зазвичай не отримує ніякої користі, якщо ви не оголосите кожен вхідний параметр як restrict(у тому числі thisдля функцій-членів).
Містичний

1
@Mystical: Потрібно пам’ятати, що charпсевдоніми мають всі типи, тож якщо у вас є знак *, то вам доведеться використовувати restrictвсе. Або якщо ви змусили стримувати чіткі правила дозволу GCC, -fno-strict-aliasingтоді все вважається можливим псевдонімом.
Зан Лінкс

1
@Ray Найновіша пропозиція щодо restrictподібної семантики в C ++ - це N4150 .
ТК

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-строгий-aliasing", і знову побачив різницю у часі.


4
Здається, це має бути коментарем, хоча це технічно відповідає на питання. Також зауважте, на жаль, ви не продемонстрували, що зв'язання було річчю. Це здається, ймовірно, правдоподібним, але це інакше, ніж робити висновок, що це було все.
SirGuy

@GuyGreer: Дивіться мою [редагувати 2] - тепер я думаю, що це доволі добре доведено.
Ярон Коен-Тал

2
Мені просто цікаво, чому ви почали використовувати змінну "i" the, коли у вас в циклі є "x"?
Jesper Madsen

1
Чи мені просто знайти фразу на 54% швидше важко зрозуміти? Ви маєте на увазі, що це в 1,54 рази швидкість неоптимізованого або щось інше?
Родді

3
@ YaronCohen-Tal так удвічі швидше? Вражаюче, але не те, що я зрозумів би "на 54% швидше"!
Родді

24

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

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

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

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

Зверніть увагу, що ця версія циклу дає значення x у діапазоні 1..ширина, а не діапазоні 0 .. (ширина-1). Це не має значення у вашому випадку, оскільки ви насправді не використовуєте x для чого-небудь, але це щось, що слід пам’ятати. Якщо ви хочете цикл зворотного відліку зі значеннями x в діапазоні 0 .. (ширина-1), ви можете це зробити.

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

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


2
Я бачив, що другий випадок відформатований як x --> 0, що призводить до оператора "downto". Досить смішно. PS Я не вважаю, що зміна для кінцевої умови є негативною для читабельності, насправді це може бути навпаки.
Марк Рансом

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

1
+1 Добре спостереження, хоча я заперечую, що підняття static_cast<unsigned>(bitmap->width)та використання widthзамість цього циклу насправді є поліпшенням для читабельності, оскільки зараз читачеві менше речей для розбору за рядком. Погляди інших можуть хоч і відрізнятися.
SirGuy

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

3
Якщо ви хочете написати петлі, які більше нагадують оптимальну зону, використовуйте 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)тип, можливе оптимізація кешування.


10

Спочатку задавали питання:

Чи варто її оптимізувати?

І моя відповідь на це (зібравши гарну суміш як голосів "за", так і "вниз".)

Нехай компілятор турбується про це.

Компілятор майже точно зробить кращу роботу, ніж ви. І немає жодної гарантії, що ваша "оптимізація" є кращою, ніж "очевидний" код - ви це виміряли ??

Що ще важливіше, чи є у вас докази того, що код, який ви оптимізуєте, впливає на продуктивність вашої програми?

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

Звичайно, зовсім іншим питанням буде таке:

Як я можу зрозуміти, чи варто оптимізувати фрагмент коду?

По-перше, чи потрібно запускати вашу програму чи бібліотеку швидше, ніж зараз? Чи занадто довго користувач чекає? Чи прогнозує ваше програмне забезпечення вчорашня погода замість завтрашньої?

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

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

Що ти маєш на увазі під оптимізацією?

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

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

Але часто, з «повільним» кодом, ці мікрооптимізації є останнім заходом. Перше місце, на яке слід звернути увагу, - це алгоритми та структури даних. Чи взагалі існує спосіб уникнути виконання роботи? Чи можна лінійні пошукові запити замінити на двійкові? Чи пов'язаний тут список швидше, ніж вектор? Або хеш-таблицю? Чи можна кешувати результати? Прийняття хороших «ефективних» рішень тут часто може вплинути на ефективність на порядок і більше!


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

4
@MarkRansom частково погодився: Але "найкращими методами" буде або: використовувати наявну бібліотеку чи API-дзвінок для заповнення зображень, або b: дозволити GPU зробити це за вас. Це ніколи не повинно бути типом невимірної мікрооптимізації, яку пропонує ОП. І як ви знаєте, що цей код коли-небудь виконується не раз, або з растровими зображеннями, більшими за 16 пікселів ...?
Родді

@Veedrac. Оцініть обґрунтування -1. Поштовх до питання суттєво і суттєво змінився відтоді, коли я відповів. Якщо ви вважаєте, що (розгорнута) відповідь все ще не корисна, час для мене видалити ... "Чи варто ..." завжди в першу чергу ґрунтується на думці.
Родді

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

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, не вдаючись до екзотичних інструкцій. Код нижче передбачає маленьку ендіанську систему і правильно вирівняні растрові карти. TEST 0 оригінальний (9 сек), TEST 1 - швидший (3 сек). Надіюсь, хтось міг би зробити це ще швидше, а результат тесту залежав би від розміру растрової карти. Безумовно, незабаром компілятор зможе виробляти стабільно швидкий код. Я боюся, що це буде майбутнє, коли компілятором буде також AI-програміст, тому ми були б без роботи. Але поки що просто напишіть код, який показує, що ви знаєте, що зайва операція в циклі не потрібна.

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

Ви можете заощадити ще 25% на 64bit, якщо ви використовуєте три int64_t замість int64_t та int32_t.
Антонін Лейсек

5

Слід розглянути дві речі.

А) Як часто буде працювати оптимізація?

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

B) Чи буде це ускладнювати код утримання / усунення несправностей?

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

Це означає, що я особисто використовую приклад B.


4

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


4

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

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

Крім того, я можу думати про патологічні ситуації, коли це насправді може уповільнити код. Наприклад, розглянемо випадок, коли компілятор не міг довести, що це bitmap->widthбуло постійним у процесі. Додавши widthзмінну, ви змушуєте компілятор підтримувати локальну змінну в цій області. Якщо з певної причини платформи ця додаткова змінна перешкоджає оптимізації простору стека, можливо, доведеться реорганізувати, як вона випускає байт-коди, і створити щось менш ефективне.

Наприклад, в Windows x64, хтось зобов'язаний викликати спеціальний виклик API __chkstkв преамбулі функції, якщо функція буде використовувати більше 1 сторінки локальних змінних. Ця функція дає вікнам можливість керувати сторожовими сторінками, які вони використовують для розширення стека за потреби. Якщо ваша додаткова змінна підштовхує використання стека нижче 1 сторінки до 1 або вище сторінки, ваша функція тепер зобов'язана дзвонити __chkstkкожен раз, коли вона вводиться. Якби ви оптимізували цю петлю на повільному шляху, ви можете насправді сповільнити швидкий шлях вниз більше, ніж ви заощадили на повільному шляху!

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


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

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

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

... подолання поведінки чи хтось ними користувався, оскільки вони були потрібного розміру, щоб відповідати потребам. Було б також корисно мати explciit псевдонімні бар'єри, які в багатьох випадках можуть бути розміщені поза циклами, на відміну від неявних бар'єрів, пов'язаних з доступом до типу символів.
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це залежність і не const, і не можна вважати, що вона може не змінюватися між наступними ітераціями. Таким чином, вона не може бути оптимізована, але повинна перевірятися в кожному циклі .

У вашому оптимізованому випадку a локальній змінній призначається значення bitmap->widthв якийсь момент під час виконання програми. Компілятор може перевірити, що це насправді не змінюється.

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

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


2

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


1

Компілятор не може оптимізувати, bitmap->widthоскільки значення "" widthможе змінюватися між ітераціями. Є кілька найпоширеніших причин:

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

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

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