LINUX.ORG.RU

Зависимость скорости работы циклов от выравнивания кода

 , ,


3

5

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

00000000000016b9 <.bL11>:
    16b9:       48 8b 04 ce             mov    rax,QWORD PTR [rsi+rcx*8]
    16bd:       48 19 04 cf             sbb    QWORD PTR [rdi+rcx*8],rax
    16c1:       48 ff c1                inc    rcx
    16c4:       75 f3                   jne    16b9 <.bL11>
    16c6:       48 83 1f 00             sbb    QWORD PTR [rdi],0x0
^ вот так всё работает с «обычной» скоростью. А так (всё сдвинуто на 1 байт вниз) - на 30-35% медленнее:
00000000000016ba <.bL11>:
    16ba:       48 8b 04 ce             mov    rax,QWORD PTR [rsi+rcx*8]
    16be:       48 19 04 cf             sbb    QWORD PTR [rdi+rcx*8],rax
    16c2:       48 ff c1                inc    rcx
    16c5:       75 f3                   jne    16ba <.bL11>
    16c7:       48 83 1f 00             sbb    QWORD PTR [rdi],0x0
двигаем дальше, всё медленно, вплоть до сюда:
00000000000016c5 <.bL11>:
    16c5:       48 8b 04 ce             mov    rax,QWORD PTR [rsi+rcx*8]
    16c9:       48 19 04 cf             sbb    QWORD PTR [rdi+rcx*8],rax
    16cd:       48 ff c1                inc    rcx
    16d0:       75 f3                   jne    16c5 <.bL11>
    16d2:       48 83 1f 00             sbb    QWORD PTR [rdi],0x0
Если сдвинуть ещё на 1 байт вниз - лаги исчезают.

Собственно, если отметить первый лагающий вариант (где .bL11=0x16BA) за точку отсчёта, то итоги такие:

-7..-1 - хорошая скорость
0..11 - замедление на 30-35%
12..15 - хорошая скорость
16..23 - замедление на 15-20%
24..31 - хорошая скорость
32..32+11 - замедление на 30-35%
32+12..32+31 - хорошая скорость
Именно так, 30-35% замедление циклично повторяется через 32 байта (и дальше тоже повторяется), а вот второе замедление, сдвинутое на 16 байт от первого, и меньше по величине, и короче по байтам, и отсутствует во втором цикле. Более того, оно отсутствует и при сдвиге на 64+16, и 96+16, и 128+16, но опять появляется на 256+16 (на 512+16 его опять нет, промежуточные уже не проверял) - странно. А ещё оно появляется/исчезает в зависимости от изменений в других линкуемых модулей. Например, простое дописывание к линкеру -lm (без изменений исходников) может на него повлиять. 30-35% же замедление присутствует стабильно на своём месте.

Ещё я проверил что будет если вставлять NOP-ы внутрь цикла. Так вот, добавление например 2 NOP-ов приводит к тому, что лаги начинаются на 2 байта раньше (т.е. при той же позиции JNE как и раньше начинались), а вот заканчиваются на том же месте как раньше, т.е. при той же позиции метки (т.е. лаги от -2 до 11). Вторая зона лагов стала какой-то размазанной с усилившимся пиком в самом начале, а так же она появилась таки во втором цикле на 32+16-2.

сдвиг   время выполнения бенчмарка
-6       511.9 +- 14.8
-5       515.2 +- 12.3
-4       511.6 +- 8.9
-3       507.3 +- 7.2
-2       683.0 +- 9.2    <-- первый круг
-1       672.1 +- 12.3
0        670.5 +- 8.6
1        674.8 +- 17.1
2        669.4 +- 7.4
3        672.5 +- 11.0
4        675.0 +- 9.1
5        669.9 +- 8.9
6        675.5 +- 8.8
7        679.1 +- 7.6
8        696.9 +- 23.6
9        691.6 +- 14.8
10       680.5 +- 9.5
11       684.0 +- 9.0
12       516.5 +- 7.9
13       517.2 +- 11.6
14       792.3 +- 36.8   <--
15       605.4 +- 37.5
16       575.8 +- 24.2
17       566.0 +- 16.0
18       555.8 +- 21.6
19       562.0 +- 14.5
20       555.7 +- 8.7
21       537.2 +- 10.7
22       534.4 +- 11.2
23       549.9 +- 14.6
24       522.1 +- 10.6
25       517.8 +- 9.5
26       515.5 +- 8.4
27       517.0 +- 4.2
28       512.2 +- 7.4
29       516.8 +- 8.6
30       680.3 +- 9.2  <-- второй круг
31       675.0 +- 19.7
32       671.7 +- 8.9
33       669.2 +- 9.1
34       668.3 +- 10.8
35       667.0 +- 9.6
36       669.0 +- 8.9
37       671.0 +- 9.5
38       670.6 +- 10.8
39       676.5 +- 10.1
40       681.0 +- 10.5
41       682.1 +- 12.7
42       693.5 +- 16.7
43       689.8 +- 13.5
44       512.5 +- 4.9
45       516.9 +- 7.1
46       685.5 +- 12.3   <--
47       515.2 +- 7.6
48       519.0 +- 8.7
49       513.2 +- 7.9
50       512.4 +- 6.2
51       510.8 +- 4.7
52       510.3 +- 7.3
53       507.4 +- 7.0
54       509.2 +- 6.9

Вообще мне казалось что для более быстрой работы кода надо выравнивать начала циклов по 16-байтным границам, но тут ничего подобного незаметно, все адреса не пойми какие. Есть замеченная закономерность: когда JNE доползает до адреса 0x16c5 - лаги начинаются, когда до него доползает и начало цикла - заканчиваются. Но JNE двухбайтовый, т.е. когда он на 0x16c4 то второй его байт на 0x16c5 и лагов ещё нет.

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

★★★★★

зависимость была со времён 386, часто связана с размером строки кэша
на большинстве современных архитектур ты никогда не угадаешь, как оно работает
можешь немного положиться на simd, если алгоритм под это заточен

madcore ★★★★★
()
Ответ на: комментарий от firkax

Ну что-то там наверняка есть, pmcstat какой-нибудь. В итоге всё равно придётся смотреть на счётчики, чтобы подтвердить или опровергнуть гипотезы.

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

i-rinat ★★★★★
()

От вставления NOP'ов у вас выравнивание данных не нарушается?

И в руководстве по оптимизации под Атлон и Опертрон написано, что нужно вставлять один NOP, а если нужно два и более байт, то перед NOP вставлять от одного до трёх operand-size overrides (66h):

NOP3_OVERRIDE_NOP TEXTEQU <DB 066h,066h,090h>
NOP4_OVERRIDE_NOP TEXTEQU <DB 066h,066h,066h,090h>
NOP5_OVERRIDE_NOP TEXTEQU <DB 066h,066h,090h,066h,090h>
NOP6_OVERRIDE_NOP TEXTEQU <DB 066h,066h,090h,066h,066h,090h>
Может у вас это явления от того, что вы 2 или более подряд NOP вставляете...

Вообще мне казалось что для более быстрой работы кода надо выравнивать начала циклов по 16-байтным границам

Да, там такой совет встречается, но, там ещё написано, что Атлон смотрит 16 байтный блок кода, выровненный по 16 байт и выполняет до трёх команд из этого блока разом. Фиг его знает, какие у вас команды выполняются одновременно при каждом варианте начального адреса блока.

mky ★★★★★
()
Последнее исправление: mky (всего исправлений: 1)
Ответ на: комментарий от VIT

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

Ты не ответил на вопрос «почему». Я отвечу: их мало не потому что они крутые, почитать статей и начать писать недостижимый по перфу для компиляторов ассемблерный код может любой школьник (и тем это проще с нейронками), а потому что они не нужны. Никого не интересует даже 30% перфа, когда ещё используется питон с 30x оверхедом, тем более ценой непереносимости кода (не говоря уже об оптимизациях, которые становятся непереносимы даже между разными моделями процессоров совместимой архитектуры).

PS. Это конечно же не в поддержку бредней ынтузиаста про «код исполняемый без участия операционной системы».

anonymous
()
Ответ на: комментарий от MKuznetsov

Это подтверждает или опровергает что-то из вышесказанного, или упомянуто просто так? Так-то можно предположить что такое сейчас нужно для реверсинга, а не оптимизаций.

anonymous
()
Ответ на: комментарий от anonymous

Мейнфреймы - это отдельная ниша. Там много легаси-кода. Не только на Коболе и ПЛ/1, но и на Ассемблере. И он там довольно неплох - CISC такой, каким он должен быть, а не то, что мы видим в x86. Видно, что для людей делали.

bigbit ★★★★★
()
Ответ на: комментарий от bigbit

чё там, компилятор PDP-11 в 2026 году хорошо работает?

ЗЫ

Я бы не был сильно уверен что какой-то gcc оптимизирует под какие-то конкретные процессоры.

peregrine ★★★★★
()
Последнее исправление: peregrine (всего исправлений: 1)
Ответ на: комментарий от cobold

Да, зависимые, но, может, одновременная загрузка операндов из памяти, ведь для того, чтобы сделать sbb ячейки памяти нужно сначала загрузить из неё в АЛУ... А может вобще влияет другой кусок кода, хорошо бы ТС написал минимальный рабочий пример. Может я этот бы пример попробовал на своём Атлоне, если он ещё рабочий, чтобы понять, насколько воспроизводимо наблюдаемое в стартовом посте.

mky ★★★★★
()
Ответ на: комментарий от mky

Добавлю, почему может влиять другой код. В Software Optimization Guide for AMD Family 10h and 12h Processors написано:

For absolute optimal performance, try to limit branches to one per 16-byte code window.

Может у ТС влияет не начало цикла, а то, что его:

16c4: 75 f3 jne 16b9 <.bL11>

попадает в один 16 байтный блок с командой перехода после цикла.

Вобще, где-то в интернетах написано, что Athlon II X2 3000 МГц — это К10, но без L3-кеша, который исходно должен быть. Фиг знает, может то что наблюдает ТС исключительно Athlon II проблемы.

mky ★★★★★
()
Ответ на: комментарий от mky

Я бы попробовал начать просто с приведенного тела цикла из 4 инструкций. rdtsc, 64 итерации цикла, rdtsc. Что-то типа того что описано вот тут на 4 странице https://arxiv.org/abs/2601.11696 только там больно современные процы относительно 20 летнего атлона

cobold ★★★★★
()
Ответ на: комментарий от mky

С nop-ами надо всё проверить подробнее, но мне кажется дело не в них, а именно в адресах значимых инструкций.

Да, там такой совет встречается, но, там ещё написано, что Атлон смотрит 16 байтный блок кода, выровненный по 16 байт и выполняет до трёх команд из этого блока разом.

Так команды пересекают эти блоки же. Ну, в момент перехода от первого быстрого к второму медленному варианту распределение команд по 16-байт блокам не меняется. При дальнейшем переходе от медленного назад к быстрому прада небольшое перераспределение есть: в последнем медленном inc rcx располагается на адресах D,E,F а в следующем за ним быстром разрезано между двумя блоками.

firkax ★★★★★
() автор топика
Ответ на: комментарий от mky

Я же отделял nop-ами цикл от всего остального (128 nop-ов до метки и 128 nop-ов после jne), закономерность сохранялась.

Да, надо минимальный пример сделать.

firkax ★★★★★
() автор топика
Ответ на: комментарий от mky

Минимальный пример оказался на удивление простым - действительно почти только этот цикл.

Ну точнее может можно и ещё меньше сделать, не проверял, но сейчас уже норм.

work.asm

/* void work(u64 *a, u64 *b, size_t length, size_t count) */
work:/* args = rdi,rsi,rdx,rcx ; must keep: rbx,rsp,rbp,r12-r15 */
	mov	r8,rcx
	lea	r9,[rsi+rdx*8]
	lea	r10,[rdi+rdx*8]
	neg	rdx
.again: /* make inner loop not cross with other labels and jumps within nearest 16-bytes */
	mov	rsi,r9
	mov	rdi,r10
	mov	rcx,rdx
	.byte 0x66,0x66,0x66,0x90
	.byte 0x66,0x66,0x66,0x90
	.byte 0x66,0x66,0x66,0x90
	.byte 0x66,0x66,0x66,0x90
	clc
.bL11:
	mov	rax,[rsi+rcx*8]
	sbb	[rdi+rcx*8],rax
	inc	rcx
	jnz	.bL11
	.byte 0x66,0x66,0x66,0x90
	.byte 0x66,0x66,0x66,0x90
	.byte 0x66,0x66,0x66,0x90
	.byte 0x66,0x66,0x66,0x90
	dec	r8
	jnz	.again
	ret

test.c

#include <sys/types.h>
#include <sys/random.h>
#include <stdio.h>
#include <time.h>

typedef unsigned long long u64;

void work(u64 *a, u64 *b, size_t length, size_t count);


    __asm(".intel_syntax noprefix");
#ifdef ZEROADD
#if ZEROADD
#define _STR(X) #X
#define STR(X) _STR(X)
    __asm(".zero " STR(ZEROADD));
#endif
#endif
    __asm(".include \"work.asm\"");
    __asm(".att_syntax\n");


static unsigned get_ticks(void) {
  struct timespec ts;
  clock_gettime(CLOCK_THREAD_CPUTIME_ID,&ts);
  return ((unsigned)ts.tv_sec)*1000 + ts.tv_nsec/1000000;
}

static u64 values[100000];

#define CYCLES 6
int main(void) {
  unsigned t0, t[CYCLES], j, k;
  size_t sz;
  
  sz = 1024*CYCLES;
  if(getrandom(values, sz, 0)!=(ssize_t)sz) { printf("getrandom() ~~~\n"); return -1; }

  for(k=0; k<CYCLES; k++) {
    t0 = get_ticks();
    for(j=0; j<1000; j++) work(values+128*k, values+128*k+64, 64, 2048);
    t[k] = get_ticks()-t0;
  }

  t0 = 0;
  for(k=0; k<CYCLES; k++) t0 += t[k];
  t0 /= CYCLES;
  printf("avg=%u |", t0);
  for(k=0; k<CYCLES; k++) printf(" %u", t[k]);
  printf("\n");
}
test.sh
for i in `seq 0 64`; do
  echo -n "ZEROADD=$i "
  gcc -o test -DZEROADD=$i test.c
  ./test
done

И проверил на линуксе на Athlon II X4 - тоже есть (неудивительно, проц почти тот же самый, хотя вроде это Phenom с чем-то там бракованным/отключённым, в отличие от X2 который изначально производился как Athlon). Однако конкретные результаты чуть отличаются, уж не знаю связано ли это с экземплярами процов, с ОС или с например с оперативкой, ниже запощу бенчмарки с обоих систем.

firkax ★★★★★
() автор топика
Ответ на: комментарий от firkax

Athlon II X4 620 (2600MHz, но кажется какой-то cpufreq governor его меняет и ставит 800 при idle), Linux

На первой итерации цикл оказывается такой:

    119d:       48 8b 04 ce             mov    (%rsi,%rcx,8),%rax
    11a1:       48 19 04 cf             sbb    %rax,(%rdi,%rcx,8)
    11a5:       48 ff c1                inc    %rcx
    11a8:       75 f3                   jne    119d <.bL11>
На +0..+2,+23..+34 - замедление (первые три это хвост предыдущего)

Т.е. медленно если цикл начинается на b4..bf, разница всё так же на 30-35%.

ZEROADD=0 avg=168 | 199 162 162 162 162 162
ZEROADD=1 avg=167 | 199 162 161 162 162 161
ZEROADD=2 avg=168 | 199 162 163 162 163 162
ZEROADD=3 avg=125 | 157 119 120 119 119 119
ZEROADD=4 avg=125 | 156 120 119 119 120 119
ZEROADD=5 avg=123 | 143 119 119 120 119 119
ZEROADD=6 avg=125 | 153 120 119 119 120 119
ZEROADD=7 avg=118 | 119 118 119 118 119 118
ZEROADD=8 avg=125 | 157 120 119 120 119 119
ZEROADD=9 avg=120 | 152 114 115 114 115 114
ZEROADD=10 avg=120 | 150 115 115 114 115 115
ZEROADD=11 avg=131 | 160 125 126 126 125 126
ZEROADD=12 avg=122 | 150 116 117 117 117 116
ZEROADD=13 avg=122 | 152 117 117 116 117 117
ZEROADD=14 avg=118 | 119 118 119 118 119 119
ZEROADD=15 avg=122 | 149 117 117 116 117 117
ZEROADD=16 avg=124 | 155 118 119 118 119 118
ZEROADD=17 avg=124 | 156 119 118 119 119 118
ZEROADD=18 avg=123 | 142 120 119 119 120 119
ZEROADD=19 avg=134 | 163 129 130 129 129 129
ZEROADD=20 avg=129 | 162 123 123 124 123 124
ZEROADD=21 avg=127 | 148 124 123 123 123 124
ZEROADD=22 avg=134 | 166 128 128 128 129 127
ZEROADD=23 avg=161 | 162 161 162 162 161 162
ZEROADD=24 avg=168 | 200 161 162 162 161 162
ZEROADD=25 avg=169 | 197 164 164 164 164 164
ZEROADD=26 avg=170 | 199 164 164 164 165 164
ZEROADD=27 avg=167 | 199 162 162 161 162 161
ZEROADD=28 avg=168 | 200 163 162 162 163 162
ZEROADD=29 avg=167 | 199 160 161 161 161 161
ZEROADD=30 avg=161 | 161 163 161 161 161 161
ZEROADD=31 avg=166 | 196 161 161 161 161 160
ZEROADD=32 avg=168 | 199 162 162 162 163 162
ZEROADD=33 avg=161 | 162 161 162 162 162 162
ZEROADD=34 avg=162 | 163 162 162 163 162 162
ZEROADD=35 avg=119 | 119 120 119 119 120 119
ZEROADD=36 avg=119 | 120 119 120 119 119 120
ZEROADD=37 avg=119 | 119 120 119 119 120 119
ZEROADD=38 avg=122 | 122 123 122 122 123 122
ZEROADD=39 avg=119 | 119 121 119 118 119 118
...

Athlon II X2 3000MHz downclock to 800 MHz, FreeBSD

0000000000400992 <.bL11>:
  400992:       48 8b 04 ce             mov    (%rsi,%rcx,8),%rax
  400996:       48 19 04 cf             sbb    %rax,(%rdi,%rcx,8)
  40099a:       48 ff c1                inc    %rcx
  40099d:       75 f3                   jne    400992 <.bL11>


ZEROADD=0 avg=423 | 425 424 426 424 422 422
ZEROADD=1 avg=438 | 442 438 438 435 439 439
ZEROADD=2 avg=552 | 553 552 551 552 556 549
ZEROADD=3 avg=553 | 553 559 555 553 552 550
ZEROADD=4 avg=559 | 560 558 559 563 558 556
ZEROADD=5 avg=560 | 560 559 561 562 561 561
ZEROADD=6 avg=553 | 553 557 553 552 553 555
ZEROADD=7 avg=556 | 555 558 555 562 556 555
ZEROADD=8 avg=549 | 548 548 551 550 546 551
ZEROADD=9 avg=550 | 552 552 549 551 550 551
ZEROADD=10 avg=551 | 550 549 556 552 554 549
ZEROADD=11 avg=553 | 552 551 558 551 554 553
ZEROADD=12 avg=552 | 553 554 553 551 554 552
ZEROADD=13 avg=552 | 554 554 553 552 552 548
ZEROADD=14 avg=408 | 402 407 406 409 418 407
ZEROADD=15 avg=410 | 410 409 411 415 409 407
ZEROADD=16 avg=409 | 409 409 409 411 408 410
ZEROADD=17 avg=409 | 407 407 412 412 408 408
ZEROADD=18 avg=406 | 413 406 404 405 403 407
ZEROADD=19 avg=410 | 419 411 408 408 411 408
ZEROADD=20 avg=390 | 391 394 391 390 390 389
ZEROADD=21 avg=392 | 394 392 392 392 393 391
ZEROADD=22 avg=429 | 429 432 431 428 431 428
ZEROADD=23 avg=407 | 400 401 399 405 419 419
ZEROADD=24 avg=411 | 416 417 418 412 401 402
ZEROADD=25 avg=402 | 403 404 403 399 407 401
ZEROADD=26 avg=401 | 404 403 403 401 399 399
ZEROADD=27 avg=406 | 404 406 405 409 410 405
ZEROADD=28 avg=405 | 405 405 407 406 405 404
ZEROADD=29 avg=408 | 409 409 408 406 409 407
ZEROADD=30 avg=432 | 436 432 431 433 432 432
ZEROADD=31 avg=428 | 426 432 430 430 427 428
ZEROADD=32 avg=423 | 425 422 426 422 422 421
ZEROADD=33 avg=439 | 439 438 443 444 436 438
ZEROADD=34 avg=552 | 552 551 554 556 551 551
...

Замедление если начало 94..9f - в целом то же самое.

Однако это всё не совпадает с изначальным тестом где замедление было на ba..c5 т.е. на 6 байт ниже. Ещё не выяснил от чего зависит, но по-моему это намёк на то что границы 16-байтовых блоков с адресами заканчивающимися на 0 тут ни при чём (да и цикличность тут не 16 а 32-байтовая)

firkax ★★★★★
() автор топика
Последнее исправление: firkax (всего исправлений: 1)

для начала давай построим график зависимости скорости от адреса начала цикла а не от какой-то рандомной точки.

ckotctvo
()
Ответ на: комментарий от i-rinat

Сравнил на другом компе

ZEROADD=2 avg=162 | 162 163 162 163 162 162

 Performance counter stats for './test':

       798 780 633      branches:u                                                  
        12 299 566      branch-misses:u           #    1,54% of all branches        

       0,976028543 seconds time elapsed

       0,976143000 seconds user
       0,000000000 seconds sys


ZEROADD=3 avg=127 | 169 120 119 119 120 119

 Performance counter stats for './test':

       798 780 767      branches:u                                                  
        12 311 800      branch-misses:u           #    1,54% of all branches        

       0,768513778 seconds time elapsed

       0,764845000 seconds user
       0,004004000 seconds sys

Разницы нет ни в количестве бранчей ни в промахах.

firkax ★★★★★
() автор топика
Последнее исправление: firkax (всего исправлений: 1)

если я правильно понимаю, rcx должен быть отрицательным, чтоб этот цикл не завис. значит в каждом цикле CF после inc будет равно 0 кроме первого цикла. в чем тогда сакральный смысл использования SBB?

ckotctvo
()
Ответ на: комментарий от ckotctvo

INC не меняет CF - это сделано ещё в 8086 специально для таких циклов, CF сохраняется от SBB из прошлого цикла.

кроме первого цикла

Кроме последнего, конечно, если бы он менял.

firkax ★★★★★
() автор топика
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от firkax

Вставление nop-ов в разные места цикла (всего 4 варианта) даёт неожиданные эффекты. Поначалу не заметил и казалось что одинаково.

firkax ★★★★★
() автор топика

Не знаю, как сейчас, уже давно не программирую на ассемблере, но 20 лет назад была такая штука ,как align (align 16, align 32). Это чтобы не лепить сотни nop'ов.

Chord ★★★★★
()
Ответ на: комментарий от firkax

И даже так всё проявляется

/* void work(u64 *a, u64 *b, size_t length, size_t count) */
work:/* args = rdi,rsi,rdx,rcx ; must keep: rbx,rsp,rbp,r12-r15 */
	imul	rdx,rcx
	neg	rdx
	mov	rcx,rdx
	.byte 0x66,0x66,0x66,0x90
	.byte 0x66,0x66,0x66,0x90
	.byte 0x66,0x66,0x66,0x90
	.byte 0x66,0x66,0x66,0x90
	clc
.bL11:
	.byte 0x66,0x66,0x66,0x90
	.byte 0x66,0x66,0x66,0x90
	inc	rcx
	jnz	.bL11
	.byte 0x66,0x66,0x66,0x90
	.byte 0x66,0x66,0x66,0x90
	.byte 0x66,0x66,0x66,0x90
	.byte 0x66,0x66,0x66,0x90
	ret
И разница между разными местами вставления nop-а вроде бы исчезла или почти исчезла. Видимо то были какие-то другие эффекты.

inc rcx можно двигать - вроде не влияет. В целом важны, всё-таки, адрес начала цикла, адрес JNE и ещё какие-то невыясненные обстоятельства, из-за которых тут замедление с 0xB4 а в оригинале начиналось от 0xBA.

firkax ★★★★★
() автор топика
Последнее исправление: firkax (всего исправлений: 2)
Ответ на: комментарий от i-rinat

Другая гипотеза в треде была про кеш-промахи

Насколько я знаю, это основная причина

Chord ★★★★★
()
Ответ на: комментарий от i-rinat

Список пустой кажется.

$ perf list hw

List of pre-defined events (to be used in -e):
А что кэш-промахи, он 13 подряд идущих байт кода не может закешировать после первого прохода по нему? В последнем варианте просто пустой цикл на 131072 итераций. На проце 2600МГц 1000 таких циклов делаются за 100мс когда хорошо и за 150 когда плохо - это по 2 и 3 такта на цикл соответственно. Кэш-промахи по-моему намного больше бы съели если б они были.

Кстати интересно - в варианте где в цикле вычитание таки делается, походу скорость работы цикла такая же, а чуть более длинное время там было из-за двумерного цикла (2048*64) вместо одномерного (131072). В двумерном 2048*64 пустом скорость как с вычитанием.

firkax ★★★★★
() автор топика
Последнее исправление: firkax (всего исправлений: 4)
Ответ на: комментарий от firkax

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

firkax ★★★★★
() автор топика
Последнее исправление: firkax (всего исправлений: 1)
  • Markdown
Пустая строка (два раза Enter) начинает новый абзац. Знак '>' в начале абзаца выделяет абзац курсивом цитирования.
Внимание: прочитайте описание разметки Markdown.
Используйте Ctrl-Enter для размещения комментария