Журнал LinuxFormat - перейти на главную

LXF107:Шаблоны в Sun Studio

Материал из Linuxformat
Версия от 22:14, 1 мая 2009; Yaleks (обсуждение | вклад)

(разн.) ← Предыдущая | Текущая версия (разн.) | Следующая → (разн.)
Перейти к: навигация, поиск

Идем на дело

ЧАСТЬ 2 Мы покончили с теорией, и пришла пора взяться за дело – сегодня Станислав Механошин покажет, как использовать встраиваемые шаблоны Sun Studio в реальном проекте!

Недавно мне пришлось столкнуться с проектом, в котором изрядную долю времени занимала многократно вызываемая функция memset16, которая, как легко догадаться по названию, ведет себя аналогично стандартной функции memset, за исключением того, что записывает в память не одинаковые байты, а одинаковые слова. Библиотечная memset хорошо известна компилятору и близка к оптимальной, но с memset16 все оказалось не так легко.

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

Описывать функцию не надо, т.к. она уже используется приложением. Иными словами, мы имеем дело с прозрачной заменой определения функции:

extern uint16_t* memset16(uint16_t* s, int c, size_t len);

Итак, создадим ее шаблон:

.inline memset16,0
;/ обнулить старшую часть регистра с символом-заполнителем
movzwq %si,%rsi
;/ получить 4 копии символа в rax
movq $0x0001000100010001,%rax
imulq %rsi,%rax
;/ запомнить указатель на массив для возврата
movq %rdi,%r8
;/ количество итераций...
movq %rdx,%rcx
;/ по 4 слова за итерацию
shrq $2,%rcx
;/ заполнение содержимым rax памяти по адресу rdi
repnz
stosq
;/ заполнить остаток, не кратный 4-м словам
movq %rdx,%rcx
andq $3,%rcx
repnz
stosw
;/ возвращаемое значение – адрес массива
movq %r8,%rax
.end

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

Обратите внимание, что параметры принимаются соответственно в регистрах rdi, rsi и rdx, а возвращаемое значение помещается в регистр rax.

Теперь требуется перекомпиляция всего приложения с добавлением memory.il (имя файла с шаблоном) к, например, CFLAGS в Makefile.

Использование такого шаблона в моем случае позволило улучшить производительность приложения примерно на 15%. Однако при переходе с AMD на платформу Intel выяснилось, что использование записи по 8 байт за инструкцию – не самое выгодное решение. Поэтому для Intel-платформ была создана отдельная версия шаблона, более длинная, но использующая запись по 16 байт за один раз:

.inline memset16,0
;/ запомнить указатель на массив для возврата
movq %rdi,%r8
;/ проверка длины на ноль
testq %rdx,%rdx
;/ и выход, если ноль
je 9f
;/ обнулить старшую часть регистра с символом-заполнителем
movzwq %si,%rsi
;/ получить 4 копии символа в rsi
movq $0x0001000100010001,%rcx
imulq %rcx,%rsi
;/ если длина массива меньше 288
cmpq $288,%rdx
;/ перейти к заполнению строковыми инструкциями
jbe 5f
movq %rdi,%rcx
;/ проверка адреса массива на выравнивание на 16 байт
andq $15,%rcx
;/ переход к заполнению movdqa, если адрес выровнен
je 2f
shrq $1,%rcx
;/ если адрес нечетный, т.е. выравнивания не добиться
;/ перейти к заполнению строковыми инструкциями
jc 5f
;/ количество символов, которые необходимо записать
;/ до достижения выравнивания
negq %rcx
addq $8,%rcx
;/ и коррекция счетчика на эту величину
subq %rcx,%rdx
;/ символ-заполнитель в rax
movq %rsi,%rax
;/ и запись максимум 7 символов до достижения выравнивания
rep
stosw
;/ получаем 8 копий символа-заполнителя в xmm0
2: movdq %rsi,%xmm0
punpcklqdq %xmm0,%xmm0
;/ вычисление целого количества записей по
;/ 8 символов из xmm0, которые не переполнят массив
movq %rdx,%rcx
andq $0x38,%rcx
;/ и коррекция счетчика для проверки в конце цикла
subq %rcx,%rdx
;/ коррекция адреса массива с учетом смещения в следующем цикле
leaq -128(%rdi,%rcx,2),%rdi
;/ и вычисление адреса перехода внутрь цикла для
;/ получения количества итераций, кратного 8
shrq $3,%rcx
negq %rcx
leaq 1f(%rcx,%rcx,4),%rcx
;/ переход внутрь цикла
jmp *%rcx
;/ 1-байтная корректировка длины 1-го movdqa для корректности
перехода
nop
;/ 8 записей по 8 символов за инструкцию
3: movdqa %xmm0,(%rdi)
movdqa %xmm0,16(%rdi)
movdqa %xmm0,32(%rdi)
movdqa %xmm0,48(%rdi)
movdqa %xmm0,64(%rdi)
movdqa %xmm0,80(%rdi)
movdqa %xmm0,96(%rdi)
movdqa %xmm0,112(%rdi)
;/ корректировка указателя
1: addq $0x80,%rdi
;/ и счетчика символов
subq $0x40,%rdx
;/ и зацикливание, если он больше или равен нулю
jae 3b
;/ корректировка счетчика с учетом вычитания до цикла
4: addq $0x40,%rdx
;/ выход, если счетчик обнулился
je 9f
;/ запись остатка, не кратного 8 символам или не выровненного
5: movq %rsi,%rax
;/ количество итераций
movq %rdx,%rcx
;/ деленное на 4, т.к. Запись по 4 слова за раз
shrq $2,%rcx
;/ собственно запись в массив по 8 байт
rep
stosq
;/ остаток, не кратный 4-м словам
movq %rdx,%rcx
andq $3,%rcx
;/ запись по одному слову
rep
stosw
;/ адрес массива – возвращаемое значение функции
9: movq %r8,%rax
end

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

Имена локальных меток – это цифры от 1 до 9, причем для адресации такой метки необходимо указать суффикс b или f, что означает ссылку на ближайшую такую метку соответственно назад или вперед, как в примере выше. Таким образом, хотя возможных имен локальных меток всего девять, их можно определить практически неограниченное количество.

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

Оптимизация шаблонов

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

.inline add_int,0
addl %esi,%edi
movl %edi,%eax
.end

и тестовую функцию, складывающую три числа:

extern int add_int(int,int);
int sum(int x, int y, int z)
{
return x+add_int(y,z);
}

После компиляции с использованием команды

cc -fast -m64 -S add.c add.il 

мы можем увидеть такой ассемблер:

sum:
.CG1: subq $8,%rsp
/ ASM INLINE BEGIN: add_int
leal (%rsi,%rdx),%eax ;/ line : 5
/ ASM INLINE END
addl %edi,%eax ;/ line : 5
addq $8,%rsp ;/ line : 5
ret ;/ line : 5

Обратите внимание, что параметры y и z функции sum расположены, соответственно, в регистрах rsi и rdx, в то время как у функции add_int они ожидаются в регистрах rdi и rsi. Однако мы можем видеть, что пересылок между регистрами в данном случае не происходит. Более того, не оптимально описанные в шаблоне сложение и пересылка были превращены компилятором в одну инструкцию lea.

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

Существует, однако, случай, когда шаблон будет использован в месте вызова дословно, без изменений и оптимизаций. Это происходит, если транслятор ассемблера шаблона не понимает написанного в нем. В этом случае его текст будет вставлен в результирующий ассемблер как есть, а параметры переданы строго в соответствии с ABI. Так, например, все псевдооперации ассемблера не понимаются транслятором шаблонов. Например, вы можете захотеть вставить выравнивание перед циклом:

.align 16
1:
;/ ...
decq %rdi
jne 1b

Это допустимо и может использоваться, однако .align не понимается транслятором, т.к. эта директива известна лишь собственно программе-ассемблеру. В результате такой шаблон будет использован без оптимизации (что не означает, что вся функция, куда он будет встроен, не будет оптимизирована).

Персональные инструменты
купить
подписаться
Яндекс.Метрика