- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
LXF107:Шаблоны в Sun Studio
Материал из Linuxformat.
Sun Studio |
---|
|
Идем на дело
- ЧАСТЬ 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
На этом наш экскурс во встраиваемые шаблоны подходит к концу, однако возможности Sun Studio им, разумеется, не исчерпываются. Есть еще что-нибудь, что вы хотели бы узнать (но боялись спросить)? Черкните нам письмо на letters@linuxformat.ru и сообщите об этом!
Обратите внимание, что параметры y и z функции sum расположены, соответственно, в регистрах rsi и rdx, в то время как у функции add_int они ожидаются в регистрах rdi и rsi. Однако мы можем видеть, что пересылок между регистрами в данном случае не происходит. Более того, не оптимально описанные в шаблоне сложение и пересылка были превращены компилятором в одну инструкцию lea.
К сожалению, из-за особенностей следования фаз оптимизации операции со стеком не были удалены компилятором, как это было бы сделано, используй мы просто сложение. Это показывает, что использование встроенных шаблонов все же накладывает некоторые ограничения на оптимизацию функций, где они используются, хотя эти ограничения и сведены к минимуму.
Существует, однако, случай, когда шаблон будет использован в месте вызова дословно, без изменений и оптимизаций. Это происходит, если транслятор ассемблера шаблона не понимает написанного в нем. В этом случае его текст будет вставлен в результирующий ассемблер как есть, а параметры переданы строго в соответствии с ABI. Так, например, все псевдооперации ассемблера не понимаются транслятором шаблонов. Например, вы можете захотеть вставить выравнивание перед циклом:
.align 16 1: ;/ ... decq %rdi jne 1b
Это допустимо и может использоваться, однако .align не понимается транслятором, т.к. эта директива известна лишь собственно программе-ассемблеру. В результате такой шаблон будет использован без оптимизации (что не означает, что вся функция, куда он будет встроен, не будет оптимизирована).