Яка різниця в продуктивності між безпідписаними та підписаними цілими числами? [зачинено]


42

Мені відомо про хіт продуктивності при змішуванні підписаних інтів з поплавками.

Чи гірше змішувати неподписані вставки з плавцями?

Чи є якесь потрапляння під час змішування підписаних / неподписаних без поплавків?

Чи впливають різні розміри (u32, u16, u8, i32, i16, i8) на продуктивність? На яких платформах?


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

Відповіді:


36

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

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


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

4
@bummzack: Це стосується лише SPE, а не ЗІЗ; SPE мають дуже, е-е, особливе середовище з плаваючою точкою, а ролях все ще відносно дорогий. Крім того, витрати залишаються однаковими для цілих чисел, що підписуються, а не підписуються.

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

1
@Luis - Я намагався знайти якусь публічну документацію щодо цього, але наразі не можу її знайти. Якщо у вас є доступ до документації на Xbox360, є хороша довідка Брюса Доусона, яка висвітлює частину цього (і його взагалі дуже добре).
celion

@Luis: Я опублікував аналіз нижче, але якщо він вас задовольняє, будь ласка, дайте відповідь Селіону - все, що він сказав, правильно, все, що я зробив, запустити GCC кілька разів.

12

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

По-перше, давайте подивимося, що коштує безпідписане розширення:

unsigned char x = 1;
unsigned int y = 1;
unsigned int z;
z = x;
z = y;

Відповідна частина розбирається на (використовуючи GCC 4.4.5):

    z = x;
  27:   0f b6 45 ff             movzbl -0x1(%ebp),%eax
  2b:   89 45 f4                mov    %eax,-0xc(%ebp)
    z = y;
  2e:   8b 45 f8                mov    -0x8(%ebp),%eax
  31:   89 45 f4                mov    %eax,-0xc(%ebp)

Так що в основному те саме - в одному випадку ми переміщуємо байт, в іншому переміщуємо слово. Далі:

signed char x = 1;
signed int y = 1;
signed int z;
z = x;
z = y;

Перетворюється на:

   z = x;
  11:   0f be 45 ff             movsbl -0x1(%ebp),%eax
  15:   89 45 f4                mov    %eax,-0xc(%ebp)
    z = y;
  18:   8b 45 f8                mov    -0x8(%ebp),%eax
  1b:   89 45 f4                mov    %eax,-0xc(%ebp)

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

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

Це не переповнення стека, тому я сподіваюся, що тут ніхто не буде заявляти, що мікрооптимізація не має значення. Ігри часто працюють на дуже великих і дуже чисельних даних, тому ретельна увага до розгалуження, кастингу, планування, вирівнювання структури тощо може дати дуже критичні покращення. Кожен, хто витратив багато часу на оптимізацію коду PPC, мабуть, має принаймні одну історію жахів про завантаження магазинів. Але в цьому випадку це насправді не має значення. Розмір пам’яті вашого цілого типу не впливає на продуктивність, якщо він вирівняний і вписується в реєстр.


2
(CW тому, що це насправді лише коментар до відповіді Целіона, і тому, що мені цікаво, які зміни коду люди можуть зробити, щоб зробити його більш

Інформація про процесор PS3 легко та законно доступна, тому обговорення матеріалів процесора, що стосуються PS3, не є проблемою. До тих пір, поки Sony не зняла підтримку OtherOS, кожен бажаючий міг вставити Linux на PS3 і запрограмувати його. Графічний процесор був поза межами, але процесор (включаючи SPE) є нормальним. Навіть не маючи підтримки OtherOS, ви можете легко схопити відповідний GCC і побачити, що таке кодовий рід.
JasonD

@Jason: Я позначав свою посаду як CW, тому якщо хтось це робить, він може надати інформацію. Однак будь-кому, хто має доступ до офіційного компілятора GameOS від Sony - це насправді єдине, що має значення - ймовірно, це заборонено.

Насправді підписане ціле число дорожче на PPC IIRC. У нього є крихітний хіт продуктивності, але він є ... також багато деталей про PSU / SPU PS3 тут: jheriko-rtw.blogspot.co.uk/2011/07/ps3-ppuspu-docs.html і тут: jheriko-rtw.blogspot.co.uk/2011/03/ppc-instruction-set.html . Цікаво, що це компілятор GameOS? Це компілятор GCC чи SNC? iirc, крім речей, про які вже говорилося, підписані порівняння мають накладні витрати, коли йдеться про оптимізацію внутрішніх циклів. Я не маю доступу до документів, що описують це, хоча - і навіть якщо б я це зробив ...
jheriko

4

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

unsigned foo(unsigned a) { return a / 1024U; }

буде оптимізовано для:

unsigned foo(unsigned a) { return a >> 10; }

Але ...

int foo(int a) { return a / 1024; }

оптимізується до:

int foo(int a) {
  return (a + 1023 * (a < 0)) >> 10;
}

або в системах, де розгалуження дешеве,

int foo(int a) {
  if (a >= 0) return a >> 10;
  else return (a + 1023) >> 10;
}

Те саме стосується модуля. Це справедливо і для не-повноважень-2 (але приклад є складнішим). Якщо у вашій архітектурі немає апаратного розриву (наприклад, більшість ARM), непідписані розділи non-consts також швидше.

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

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


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

2
Можливо, на x86. На ARMv7 це просто умовно виконується.
Джон Ріплі

3

Операції з підписаним або непідписаним int мають однакову вартість на поточних процесорах (x86_64, x86, powerpc, arm). У 32-бітовому процесорі u32, u16, u8 s32, s16, s8 повинні бути однакові. Ви можете покарати покарання з поганим відчуженням.

Але перетворити int в float або float в int - це дорога операція. Ви можете легко знайти оптимізовану реалізацію (SSE2, Neon ...).

Найважливіший момент - це, мабуть, доступ до пам'яті. Якщо ваші дані не вміщуються в кеш-пам'ять L1 / L2, ви втратите більше циклу, ніж конверсія.


2

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

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

Будьте уважні щодо вирівнювання даних. У більшості реалізацій HW нестандартне завантаження та зберігання відбувається повільніше. Природне вирівнювання означає, що, наприклад, 4-байтне слово, адреса є кратною чотирьом, а вісім байтових адрес слів повинні бути кратними восьми байтам. Це переходить у SSE (128 біт сприяє вирівнюванню 16 байт). AVX незабаром розширить ці "векторні" розміри регістрів до 256 біт, а потім 512 біт. І вирівняні вантажі / магазини будуть швидшими, ніж нестандартні. Для вишукачів HW нестандартна операція з пам'яттю може охоплювати такі речі, як кешлін та навіть межі сторінки, для чого HW повинен бути обережним.


1

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

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

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


0

Як вказує celion, накладні перетворення між int і floats значною мірою стосуються копіювання та перетворення значень між регістрами. Єдині накладні витрати неподписаних вхідних даних самі по собі випливають із гарантованої поведінки навколо них, що вимагає певної кількості перевірки переповнення у складеному коді.

В основному немає накладних витрат при перетворенні між підписаними і непідписаними цілими числами. Різні розміри цілого числа можуть бути (нескінченно мало) швидшими або повільнішими для доступу в залежності від платформи. Взагалі кажучи, розмір цілого числа, найбільш близький до розміру слова платформи, буде найшвидшим для доступу, але загальна різниця в продуктивності залежить від багатьох інших факторів, особливо помітний розмір кешу: якщо ви використовуєте, uint64_tколи все, що вам потрібно uint32_t, він може якщо менше ваших даних збирається відразу в кеш, і ви можете зазнати деякого завантаження накладних витрат.

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


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

@JoeWreschnig: Чорт. Я не можу його знайти, але я знаю, що я бачив приклади різного виходу асемблера для визначеної поведінки навколо, принаймні, на певних платформах. Єдиний пов'язаний пост, який я міг знайти: stackoverflow.com/questions/4712315/…
Джон Перді,

Різний вихід асемблера для різної поведінки накручування полягає в тому, що компілятор може зробити оптимізацію в підписаному випадку, наприклад, якщо b> 0, то a + b> a, оскільки переповнення підпису не визначено (і тому на нього не можна покластися). Це справді зовсім інша ситуація.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.