http://wiki.linuxformat.ru/wiki/index.php?title=LXF107:%D0%A8%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD%D1%8B_%D0%B2_Sun_Studio&feed=atom&action=historyLXF107:Шаблоны в Sun Studio - История изменений2024-03-29T08:20:16ZИстория изменений этой страницы в викиMediaWiki 1.19.20+dfsg-0+deb7u3http://wiki.linuxformat.ru/wiki/index.php?title=LXF107:%D0%A8%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD%D1%8B_%D0%B2_Sun_Studio&diff=7753&oldid=prevYaleks: Новая: {{Цикл/Sun Studio}} == Идем на дело == : ''ЧАСТЬ 2 Мы покончили с теорией, и пришла пора взяться за дело – сегодня ...2009-05-01T19:14:13Z<p>Новая: {{Цикл/Sun Studio}} == Идем на дело == : ''ЧАСТЬ 2 Мы покончили с теорией, и пришла пора взяться за дело – сегодня ...</p>
<p><b>Новая страница</b></p><div>{{Цикл/Sun Studio}}<br />
== Идем на дело ==<br />
: ''ЧАСТЬ 2 Мы покончили с теорией, и пришла пора взяться за дело – сегодня '''Станислав Механошин''' покажет, как использовать встраиваемые шаблоны Sun Studio в реальном проекте!''<br />
<br />
Недавно мне пришлось столкнуться с проектом, в котором<br />
изрядную долю времени занимала многократно вызываемая<br />
функция memset16, которая, как легко догадаться по названию, ведет себя аналогично стандартной функции memset, за исключением того, что записывает в память не одинаковые байты, а одинаковые<br />
слова. Библиотечная memset хорошо известна компилятору и близка к<br />
оптимальной, но с memset16 все оказалось не так легко.<br />
<br />
Мне требовалось не только написать оптимальный код для этой<br />
функции, но и добиться ее встраивания по месту вызова, чтобы уменьшить накладные расходы на сам вызов, т.к. результаты профилировки<br />
показали, что данная функция вызывается часто и обычно с небольшим<br />
количеством данных. При подобной постановке задачи выбор определенно падает на встраиваемые шаблоны.<br />
<br />
Описывать функцию не надо, т.к. она уже используется приложением. Иными словами, мы имеем дело с прозрачной заменой определения<br />
функции:<br />
<source lang="C">extern uint16_t* memset16(uint16_t* s, int c, size_t len);</source><br />
Итак, создадим ее шаблон:<br />
<source lang="asm">.inline memset16,0<br />
;/ обнулить старшую часть регистра с символом-заполнителем<br />
movzwq %si,%rsi<br />
;/ получить 4 копии символа в rax<br />
movq $0x0001000100010001,%rax<br />
imulq %rsi,%rax<br />
;/ запомнить указатель на массив для возврата<br />
movq %rdi,%r8<br />
;/ количество итераций...<br />
movq %rdx,%rcx<br />
;/ по 4 слова за итерацию<br />
shrq $2,%rcx<br />
;/ заполнение содержимым rax памяти по адресу rdi<br />
repnz<br />
stosq<br />
;/ заполнить остаток, не кратный 4-м словам<br />
movq %rdx,%rcx<br />
andq $3,%rcx<br />
repnz<br />
stosw<br />
;/ возвращаемое значение – адрес массива<br />
movq %r8,%rax<br />
.end</source><br />
В данном случае я использую строковые функции для записи памяти, причем для повышения производительности первая часть функции<br />
записывает по 8 байт за раз, и лишь не кратный восьми остаток пишет<br />
в память словами.<br />
<br />
Обратите внимание, что параметры принимаются соответственно в регистрах rdi, rsi и rdx, а возвращаемое значение помещается в регистр rax.<br />
<br />
Теперь требуется перекомпиляция всего приложения с добавлением<br />
memory.il (имя файла с шаблоном) к, например, CFLAGS в Makefile.<br />
<br />
Использование такого шаблона в моем случае позволило улучшить<br />
производительность приложения примерно на 15%. Однако при переходе с AMD на платформу Intel выяснилось, что использование записи по<br />
8 байт за инструкцию – не самое выгодное решение. Поэтому для Intel-платформ была создана отдельная версия шаблона, более длинная, но<br />
использующая запись по 16 байт за один раз:<br />
<source lang="asm">.inline memset16,0<br />
;/ запомнить указатель на массив для возврата<br />
movq %rdi,%r8<br />
;/ проверка длины на ноль<br />
testq %rdx,%rdx<br />
;/ и выход, если ноль<br />
je 9f<br />
;/ обнулить старшую часть регистра с символом-заполнителем<br />
movzwq %si,%rsi<br />
;/ получить 4 копии символа в rsi<br />
movq $0x0001000100010001,%rcx<br />
imulq %rcx,%rsi<br />
;/ если длина массива меньше 288<br />
cmpq $288,%rdx<br />
;/ перейти к заполнению строковыми инструкциями<br />
jbe 5f<br />
movq %rdi,%rcx<br />
;/ проверка адреса массива на выравнивание на 16 байт<br />
andq $15,%rcx<br />
;/ переход к заполнению movdqa, если адрес выровнен<br />
je 2f<br />
shrq $1,%rcx<br />
;/ если адрес нечетный, т.е. выравнивания не добиться<br />
;/ перейти к заполнению строковыми инструкциями<br />
jc 5f<br />
;/ количество символов, которые необходимо записать<br />
;/ до достижения выравнивания<br />
negq %rcx<br />
addq $8,%rcx<br />
;/ и коррекция счетчика на эту величину<br />
subq %rcx,%rdx<br />
;/ символ-заполнитель в rax<br />
movq %rsi,%rax<br />
;/ и запись максимум 7 символов до достижения выравнивания<br />
rep<br />
stosw<br />
;/ получаем 8 копий символа-заполнителя в xmm0<br />
2: movdq %rsi,%xmm0<br />
punpcklqdq %xmm0,%xmm0<br />
;/ вычисление целого количества записей по<br />
;/ 8 символов из xmm0, которые не переполнят массив<br />
movq %rdx,%rcx<br />
andq $0x38,%rcx<br />
;/ и коррекция счетчика для проверки в конце цикла<br />
subq %rcx,%rdx<br />
;/ коррекция адреса массива с учетом смещения в следующем цикле<br />
leaq -128(%rdi,%rcx,2),%rdi<br />
;/ и вычисление адреса перехода внутрь цикла для<br />
;/ получения количества итераций, кратного 8<br />
shrq $3,%rcx<br />
negq %rcx<br />
leaq 1f(%rcx,%rcx,4),%rcx<br />
;/ переход внутрь цикла<br />
jmp *%rcx<br />
;/ 1-байтная корректировка длины 1-го movdqa для корректности<br />
перехода<br />
nop<br />
;/ 8 записей по 8 символов за инструкцию<br />
3: movdqa %xmm0,(%rdi)<br />
movdqa %xmm0,16(%rdi)<br />
movdqa %xmm0,32(%rdi)<br />
movdqa %xmm0,48(%rdi)<br />
movdqa %xmm0,64(%rdi)<br />
movdqa %xmm0,80(%rdi)<br />
movdqa %xmm0,96(%rdi)<br />
movdqa %xmm0,112(%rdi)<br />
;/ корректировка указателя<br />
1: addq $0x80,%rdi<br />
;/ и счетчика символов<br />
subq $0x40,%rdx<br />
;/ и зацикливание, если он больше или равен нулю<br />
jae 3b<br />
;/ корректировка счетчика с учетом вычитания до цикла<br />
4: addq $0x40,%rdx<br />
;/ выход, если счетчик обнулился<br />
je 9f<br />
;/ запись остатка, не кратного 8 символам или не выровненного<br />
5: movq %rsi,%rax<br />
;/ количество итераций<br />
movq %rdx,%rcx<br />
;/ деленное на 4, т.к. Запись по 4 слова за раз<br />
shrq $2,%rcx<br />
;/ собственно запись в массив по 8 байт<br />
rep<br />
stosq<br />
;/ остаток, не кратный 4-м словам<br />
movq %rdx,%rcx<br />
andq $3,%rcx<br />
;/ запись по одному слову<br />
rep<br />
stosw<br />
;/ адрес массива – возвращаемое значение функции<br />
9: movq %r8,%rax<br />
end</source><br />
Здесь следует обратить внимание на использование меток. Хотя в<br />
шаблоне и можно использовать обычные текстовые метки, этого лучше<br />
не делать. Дело в том, что текст шаблона вставляется в получившийся ассемблер компилируемого модуля практически дословно. Если вы<br />
используете шаблон только один раз в единице трансляции, ничего<br />
страшного не случится; однако если вы вызовете такую функцию хотя<br />
бы дважды, то получите сообщение о повторно определенных символах.<br />
Здесь на помощь приходят локальные метки, для которых каждый раз<br />
создается новый уникальный символ.<br />
<br />
Имена локальных меток – это цифры от 1 до 9, причем для адресации такой метки необходимо указать суффикс b или f, что означает<br />
ссылку на ближайшую такую метку соответственно назад или вперед,<br />
как в примере выше. Таким образом, хотя возможных имен локальных<br />
меток всего девять, их можно определить практически неограниченное<br />
количество.<br />
<br />
Небольшое замечание по использованию имен функций: имя, указанное в шаблоне, должно быть таким, как того ожидает компилятор, т.е.<br />
со всеми декорациями, присущими языку.<br />
<br />
=== Оптимизация шаблонов ===<br />
При использовании высоких уровней оптимизации, текст шаблона<br />
совершенно не обязательно будет вставлен в программу «как есть».<br />
Ассемблер шаблона так же участвует в платформенно-зависимых оптимизациях, осуществляемых кодогенератором, как и остальное тело<br />
функции. В частности, регистры, через которые передаются параметры и возвращается вычисляемое значение, будут выбраны исходя из<br />
того факта, чтобы они уже содержали нужные переменные к моменту<br />
использования шаблона. Например, рассмотрим простой шаблон, складывающий два числа:<br />
<source lang="asm">.inline add_int,0<br />
addl %esi,%edi<br />
movl %edi,%eax<br />
.end</source><br />
и тестовую функцию, складывающую три числа:<br />
<source lang="C">extern int add_int(int,int);<br />
int sum(int x, int y, int z)<br />
{<br />
return x+add_int(y,z);<br />
}</source><br />
После компиляции с использованием команды <br />
cc -fast -m64 -S add.c add.il <br />
мы можем увидеть такой ассемблер:<br />
<source lang="asm">sum:<br />
.CG1: subq $8,%rsp<br />
/ ASM INLINE BEGIN: add_int<br />
leal (%rsi,%rdx),%eax ;/ line : 5<br />
/ ASM INLINE END<br />
addl %edi,%eax ;/ line : 5<br />
addq $8,%rsp ;/ line : 5<br />
ret ;/ line : 5</source><br />
{{Врезка<br />
|Заголовок=Что дальше?<br />
|Содержание=На этом наш экскурс во встраиваемые шаблоны<br />
подходит к концу, однако возможности Sun<br />
Studio им, разумеется, не исчерпываются. Есть<br />
еще что-нибудь,<br />
что вы хотели<br />
бы узнать (но<br />
боялись спросить)? Черкните<br />
нам письмо<br />
на [mailto:letters@linuxformat.ru letters@linuxformat.ru]<br />
и сообщите об<br />
этом!<br />
|Ширина=150px}}<br />
Обратите внимание, что параметры y и z функции sum расположены,<br />
соответственно, в регистрах rsi и rdx, в то время как у функции add_int<br />
они ожидаются в регистрах rdi и rsi. Однако мы можем видеть, что пересылок между регистрами в данном случае не происходит. Более того, не<br />
оптимально описанные в шаблоне сложение и пересылка были превращены компилятором в одну инструкцию lea.<br />
<br />
К сожалению, из-за особенностей следования фаз оптимизации<br />
операции со стеком не были удалены компилятором, как это было бы<br />
сделано, используй мы просто сложение. Это показывает, что использование встроенных шаблонов все же накладывает некоторые ограничения<br />
на оптимизацию функций, где они используются, хотя эти ограничения<br />
и сведены к минимуму.<br />
<br />
Существует, однако, случай, когда шаблон будет использован в<br />
месте вызова дословно, без изменений и оптимизаций. Это происходит, если транслятор ассемблера шаблона не понимает написанного в<br />
нем. В этом случае его текст будет вставлен в результирующий ассемблер как есть, а параметры переданы строго в соответствии с ABI. Так,<br />
например, все псевдооперации ассемблера не понимаются транслятором шаблонов. Например, вы можете захотеть вставить выравнивание<br />
перед циклом:<br />
<source lang="asm">.align 16<br />
1:<br />
;/ ...<br />
decq %rdi<br />
jne 1b</source><br />
Это допустимо и может использоваться, однако .align не понимается<br />
транслятором, т.к. эта директива известна лишь собственно программе-ассемблеру. В результате такой шаблон будет использован без оптимизации (что не означает, что вся функция, куда он будет встроен, не будет<br />
оптимизирована).</div>Yaleks