Нові оптимізації для х86 в GCC 5.0: PIC в 32-бітному режимі

Даний пост продовжує серію з трьох статей про оптимізацію для x86 в GCC 5.0. У попередній статті йшлося про векторизації. Нагадаю, що GCC 5.0 зараз знаходиться у фазі stage3, тобто впровадження нових оптимізацій вже фактично заверешено і рівень продуктивності за рідкісним винятком залишиться колишнім і в продуктовому релізі. Сьогодні мова піде про прискореннях позиційно-незалежного коду або position independent code (PIC) в 32-бітному режимі для x86.

PIC (відповідно вікіпедії) — це програма, яка може бути розміщена в будь-якій області пам'яті, так як всі посилання на комірки пам'яті в ній відносні. Такий спосіб компіляції програми використовується для Android, бібліотек і багатьох інших додатків. Більшість додатків під Android зараз є 32-бітними, так що продуктивність GCC для PIC в 32-бітному режимі дуже важлива.

Очікується, що GCC 5.0 істотно (до 30%) розжене програми, де продуктивність зосереджена в цілочисельному циклі, а саме, такі як криптографія, захист даних від перешкод, стиснення даних, хешування та інші, особливо ті, де векторизація по тим чи іншим причинам не применилась.

Що ж змінилося в GCC 5.0 порівняно з GCC 4.9?

У GCC 4.9 регістр EBX зарезервований для адреси глобальній таблиці зміщень або global offset table (GOT) і, отже, недуступен для розподілу. Таким чином, для PIC до 32х бітному режимі доступно тільки 6 регістрів (замість звичайних 7): EAX, ECX, EDX, ESI, EDI і EBP. Це призводить до суттєвих втрат продуктивності, коли для розподілу не вистачає регістрів.

У GCC 5.0 регістр EBX доступний для розподілу. Таким чином, загальна кількість вільних ригстров для PIC не відрізняється від абсолютного коду. Нижче наведені результати для тесту з цілочисельними обчисленнями в циклі з браком регістрів.

int i, j, k; 
uint32 *in = a, *out = b; 
for (i = 0; i < 1024; i++) 
{ 
for (k = 0; k < ST; k++) 
{ 
uint32 s = 0; 
for (j = 0; j < LD; j++) 
s += (in[j] * c[j][k] + 1) >> j + 1; 
out[k] = s; 
} 
in += LD; 
out += ST; 
} 

Де:
  • c — це електронний матриця:

const byte c[8][8] = {1, -1, 1, -1, 1, -1, 1, -1, 
1, 1, -1, -1, 1, 1, -1, -1, 
1, 1, 1, 1, -1, -1, -1, -1, 
-1, 1, -1, 1, -1, 1, -1, 1, 
-1, -1, 1, 1, -1, -1, 1, 1, 
-1, -1, -1, -1, 1, 1, 1, 1, 
-1, -1, -1, 1, 1, 1, -1, 1, 
1, -1, 1, 1, 1, -1, -1, -1}; 

Така матриця використовується, щоб мінімізувати обчислення всередині циклу до порівняно швидких додавань і вычитаний, але збільшити кількість залежностей.
  • in та out — покажчики на глобальні масиви «a[1024 * LD] і b[1024 * ST]»
  • uint32 — це unsigned int
  • LD та ST — макроси, що визначають довжину групи завантажень з пам'яті і збережень в пам'ять відповідно
Опції компіляції "-Ofast-funroll-loops-fno-tree-vectorize --param max-completely-peeled-insns=200" плюс "-march=slm" Silvermont, "-march=core-avx2" Haswell, "-fPIC" для PIC і "-DLD={4, 5, 6, 7, 8} -DST=7"

"-fno-tree-vectorize" — використовується, щоб уникнути векторизації і, отже, використання xmm регістрів (яких завжди доступно однакову кількість)
"--param max-completely-peeled-insns=200" — використовується щоб GCC 5.0 і 4.9 були в рівних умовах, так як для 4.9 цей параметр дорівнював 100

Приріст продуктивності GCC 5.0 порівняно з 4.9 (в скільки разів прискорилося, вище — краще).
По осі Х змінюється кількість завантажень в циклі: LD. Більша кількість «LD» веде до більшого регистровому тиску.
image

Тут ми бачимо, що і Silvermont, і Haswell показують значний приріст. Але щоб підтвердити, що це сталося саме через додавання до розподілу EBX регістра слід звернутися до 2 чартам нижче:

Ці чарти відображають уповільнення від переходу до PIC для Haswell і Silvermont на компіляторах GCC 5.0 і GCC 4.9 (вище — краще)
image
image

Тут видно, що GCC 5.0 не сильно програє від переходу до PIC. GCC 4.9 навпаки сповільнюється досить суттєво як на Haswell, так і на Silvermont. Це підтверджує, що GCC 5.0 повинен сильно прискорити цілочисельні цикли для PIC. Більш того, розробники зможуть використовувати більш агресивні оптимізації (збільшують реєстрове тиск), такі як розкрутка циклів (unroll), підстановка функцій (inline), більш агресивний винос інваріанти…

Спробувати GCC 5.0 можна вже зараз. Можливо і портування Android NDK.

Процесори використані у вимірах:
Silvermont: Intel® Atom(TM) CPU C2750 @ 2.41 GHz
Haswell: Intel® Core(TM) i7-4770K CPU @ 3.50 GHz

Компілятори використовувані у вимірах:
Завантажити приклад, на якому проводилися заміри, можна з оригінального тексту статті англійською.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.