Содержание


Расширенные возможности встроенного ассемблера для Linux на платформе z Systems

Повышение производительности приложений платформы IBM z Systems с использованием встроенного ассемблера в компиляторе IBM XL C/C

Comments

Компилятор IBM XL C/C++ для Linux на платформе z Systems версии 1.1, выпущенный в 2015 году, позволяет непосредственно встраивать ассемблерные инструкции (ассемблерный код) в приложения, разрабатываемые на языке C/C++. Благодаря этому опытные разработчики могут создавать более эффективные приложения, используя инструкции на уровне процессора. Встроенный ассемблер позволяет разработчикам ПО использовать ассемблерный код в наиболее критичных частях своих программ на C/C++, раскрывая весь свой потенциал и обеспечивая наилучшее быстродействие своих программ.

Цель данной статьи – познакомить читателей с расширенными возможностями встроенного ассемблера, поддерживаемого компилятором IBM XL для Linux на платформе z Systems. В статье подробно рассмотрены ассемблерные метки, абсолютные и относительные переходы, символические имена для входных и выходных операндов, ограничители совпадения, а также регистры из списка экранирования. В статье рассматриваются инструкции ассемблера, использующие основные регистры. Векторные регистры и регистры с плавающей точкой будут рассмотрены отдельно. Статья адресована опытным разработчикам, которые используют компилятор для Linux на платформе z Systems и хотят расширить возможности оптимизации своих высокопроизводительных приложений.

Ассемблерные метки

В процессе компиляции в объектном файле создаются внутренние имена (метки) для всех переменных и функций, объявленных в коде приложения. Эти имена также используются для обращений к соответствующим переменным и функциям в ассемблерном коде. Ассемблерные метки позволяют управлять внутренними именами переменных и функций в объектном файле. После завершения генерации ассемблерного кода имя, указанное с помощью ассемблерной метки, становится именем соответствующей переменной или функции. Таким образом, объявление вида int func( ) asm ("my_function") означает, что функция func в объектном файле будет называться my_function, а не _func, как в традиционном случае.

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

В листингах 1 и 2 содержатся фрагменты кода (модули label_b.c и label_a.c на языке C) демонстрирующие использование ассемблерной метки для прототипа функции.

Листинг 1. Модуль label_b.c – определение функции func_asm
int func_asm() {        // здесь определяется функция func_asm
    return 55;
}

В модуле label_b.c определяется функция func_asm().

Листинг 2. Модуль label_a.c – связывание имени функции с ассемблерной меткой
int func() asm("func_asm");        // функция связывается с меткой “func_asm”
int main() {
   return func();                         // вызов функции func
}

Ассемблерный оператор в строке 1 модуля label_a.c связывает функцию func с именем func_asm. В строке 3 происходит вызов функции func(), хотя она не определена в этом модуле. Предполагается, что функция func связана с именем func_asm, а вызов функции func() будет преобразован в вызов функции func_asm().

Компиляция, сборка и запуск модулей label_a.c и label_b.c не должны вызвать проблем. В результате запуска исполняемого файла мы получили значение 55, поскольку символ func связан с именем func_asm. На рисунке 1 показан сгенерированный для модуля label_a.c ассемблерный код, который подтверждает, что вместо func: [ BRASL %r14, func_asm ] использовалось имя func_asm.

Рисунок 1. Вызов функции func_asm вместо функции func в модуле label_a.c
Вызов функции func_asm вместо функции func в модуле label_a.c
Вызов функции func_asm вместо функции func в модуле label_a.c

Переход по метке

Существует два вида переходов по метке: абсолютные и относительные. В первом случае переход по метке осуществляется при выполнении определенного условия, а метка должна быть определена в приложении и иметь уникальное имя. Во втором случае переход выполняется по метке относительно расположения инструкции ветвления. Если целевая метка расположена перед инструкцией ветвления, то к адресу ветвления добавляется символ b (backward – переход в обратном направлении). Аналогично, если целевая метка расположена после инструкции ветвления, то к адресу ветвления добавляется символ f (forward – переход в прямом направлении).

Абсолютные переходы

В листинге 3 представлен пример абсолютного перехода.

Листинг 3. Пример абсолютного перехода
int absoluteValue(int a) {
     asm (" CFI %0, 0\n"
          " BRC 0xA, DONE\n"
          " LCR %0, %0\n"
          " DONE:\n"
          :"+r"(a)
         );
    return a;
}

В таблице 1 приводится сопоставление значений кода условий и маски для инструкции CFI (Compare Immediate) из второй строки листинга 3.

Таблица 1. Сопоставление значений кода условий и маски для инструкции CFI
Сравнение переменной a с числом 0Код условияБиты маски
a = 0 0 = 002 1000
a < 0 1 = 012 0100
a > 0 2 = 102 0010

Во второй строке листинга 3 переменная a (%0) сравнивается с числом 0 при помощи инструкции CFI. Если выполняется условие a == 0 или a > 0, код условия принимает значение 0 или 2 (согласно строкам 2 и 4 таблицы 1). Комбинированным значением битовой маски для кодов условий 0 и 2 является 10102. В шестнадцатеричной системе счисления значение 10102 записывается как 0xA. Соответственно, при выполнении условия a >= 0 инструкция ветвления в строке 3 передает управление по метке DONE (строка 5). Функция возвращает значение переменной a, а инструкция LCR в строке 4 не выполняется. С другой стороны, если выполняется условие a < 0, ветвления не происходит. В этом случае инструкция LCR в строке 4 загружает в переменную a ее поразрядное дополнение, после чего возвращает ее значение. Таким образом, функция всегда возвращает абсолютное значение переменной a. В этом примере абсолютный переход по метке DONE используется для пропуска инструкции LCR в строке 4.

Относительные переходы

В листинге 4 представлен пример относительного обратного перехода.

Листинг 4. Псевдокод, содержащий относительный переход
asm ( "1:          \n"
        "DoSomeWork\n"
        "BRCT  %0, 1b  \n"
        :"+r"(limit)
       );

Инструкция BRCT (Branch Relative On Count) уменьшает значение первого операнда limit (%0) на 1 и сохраняет результат в этом же операнде. Если результат не равен нулю, выполняется переход по адресу, указанному во втором операнде 1b, т. е. по метке 1 в обратном направлении. Метка 1 в строке 1 предшествует инструкции ветвления. Таким образом, инструкция BRCT уменьшает значение переменной limit и передает управление в метку 1 до тех пор, пока значение limit не будет равняться нулю, после чего цикл завершается.

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

Символические имена

Существует возможность определять входные и выходные операнды с помощью символических имен, к которым можно обращаться из ассемблерного кода. Символическое имя определяется внутри квадратных скобок перед строкой ограничителя. При обращении к символическим именам из ассемблерного кода можно использовать конструкцию %[символическое_имя] (вместо традиционной конструкции %номер_операнда). Символические имена могут являться любыми допустимыми именами переменных языка C, даже если переменные с такими именами были объявлены в коде C. В то же время символические имена должны быть уникальными внутри каждого ассемблерного оператора.

Пример из листинга 5 демонстрирует использование символических имен [results], [first] и [second] для обращения к нулевому, первому и второму операндам соответственно. Вместо обращения к операндам %0, %1 и %2 инструкции оператора обращаются к символическим именам %[result], %[first] и %[second].

Листинг 5. Пример использования символических имен
int main(){
   int sum = 0, one=1, two = 2; 
   asm ("AR  %[result], %[first]\n"  
             "AR  %[result], %[second]\n"  
             :[result] "+r"(sum)
             :[first]  "r"(one), [second] "r"(two) 
            );
   return sum == 3 ? 0 : 1;
}

Ограничители совпадения

Ограничители совпадения 0, 1, …, 9 рекомендуют компилятору выделять один и тот же регистр для входного операнда и для нумерованного выходного операнда. Таким образом, ограничители совпадения можно использовать только совместно с входными операндами. Это необходимо, когда в качестве входных данных какой-либо операции используется результат предыдущей операции. Если не задействовать ограничители совпадения, компилятор не узнает о том, что для входного и выходного операндов необходимо использовать один и тот же регистр.

В модуле example07a.c (листинг 6) приведен пример, в котором программа выдает неправильный результат по причине отсутствия ограничителя совпадения.

Листинг 6. Модуль example07a.c – некорректный результат при отсутствии ограничителя совпадения
#include <stdio.h>
int main () {
   int a = 10, b = 200, c = 3000;
   printf ("INITIAL: a = %d, b = %d, c = %d\n", a, b, c );
   asm ("LR %0, %2\n"
             "LR %1, %3\n"
            :"=r"(a),"=r"(b)
            :"r"(c), "r"(a));
   printf ("RESULT : a = %d, b = %d, c = %d\n", a, b, c );
   return 0;
}

В первой инструкции LR (Load Registers), выполняющейся в строке 5, в переменную a загружается значение переменной c. Поскольку переменная c имеет значение 3000, переменная a также будет иметь значение 3000. Далее в строке 6 выполняется следующая инструкция LR, в которой в переменную b загружается значение переменной a. Если по замыслу разработчика в переменную b должно загружаться значение переменной a, равное 3000 (т. е. значение после выполнения первой инструкции LR), то модуль example07a.c не выдаст этот результат. Нет никакой гарантии того, что при выполнении двух инструкций LR компилятор будет использовать для переменной a один и тот же регистр. Если этого не произойдет, то в переменную b будет загружено предыдущее значение переменной a, т. е. 10. Из листинга 7 видно, что модуль example07a.c в большинстве случаев возвращает неправильное значение переменной b (10 вместо 3000).

Листинг 7. Компиляция и запуск модуля example07a.c
xlc -o example07a example07a.c;
./example07a
INITIAL: a = 10, b = 200, c = 3000
RESULT : a = 3000, b = 10, c = 3000      <- b is loaded with a, but b is 10 while a is 3000

Поскольку нам требуется загрузить в переменную b обновленное значение переменной a, мы должны использовать ограничитель совпадения, благодаря чему результат выполнения инструкции LR из строки 5 будет являться входными данными для инструкции LR в строке 6. В этом случае при выполнении обеих инструкций LR компилятор будет использовать для переменной a один и тот же регистр. Модуль example07b.c (листинг 8) содержит исправленный код с использованием ограничителя совпадения.

Листинг 8. Модуль example07b.c – исправленный код с использованием ограничителя совпадения
#include <stdio.h>
int main () {
   int a = 10, b = 200, c = 3000;
   printf ("INITIAL: a = %d, b = %d, c = %d\n", a, b, c );
   asm ("LR %0, %2\n"
             "LR %1, %3\n"
            :"=r"(a),"=r"(b)
            :"r"(c), "0"(a));
   printf ("RESULT : a = %d, b = %d, c = %d\n", a, b, c );
   return 0;
}

В строке 8 модуля example07b.c используется ограничитель совпадения "0"(a), сообщающий компилятору о том, что для входного операнда a (%3) необходимо использовать тот же самый регистр, что и для нулевого выходного операнда a. Поскольку первая инструкция LR (строка 5) загружает в переменную a значение переменной c, равное 3000, а вторая инструкция LR (строка 6) использует для входного операнда a этот же регистр, то в переменную b, загружается значение 3000, как и ожидалось.

На рисунке 4 показаны различия между двумя ассемблерными файлами, сгенерированными для модулей example07a.c (слева) и example07b.c (справа). В случае отсутствия ограничителя совпадения (модуль example07a.c) ясно видно, что компилятор использует для выходного операнда a и входного операнда a разные регистры – r1 и r5, соответственно. При использовании ограничителя совпадения (модуль example07a.c) обе операции LR работают с одним и тем же регистром r1.

Рисунок 2. Генерация ассемблерного кода в зависимости от наличия ограничителя совпадения
Генерация ассемблерного кода в зависимости от наличия ограничителя совпадения
Генерация ассемблерного кода в зависимости от наличия ограничителя совпадения

В таблице 2 объясняется работа модуля example07a.c, когда ограничитель совпадения не используется. Обновление данных с использованием регистра r1 никак не связано с входным значением, получаемым из регистра r5. Поэтому во второй инструкции LR обновленное значение не используется.

Таблица 2. Ассемблерный код при отсутствующем ограничителе совпадения (модуль example07a.c)
Ассемблерная инструкцияОбъяснение
BRASL %r14,printfвызов функции printf INITIAL …
L %r3,168(,%r15)Загрузка значения переменной c из адреса r15+168 в регистр r3: регистр r3 содержит значение 3000
L %r5,176(,%r15)Загрузка значения переменной a из адреса r15+176 в регистр r5: регистр r5содержит значение 10
#GS00000Начало встраивания пользовательской ассемблерной инструкции
LR %r1, %r3Загрузка регистра r3 (значение переменной c, равное 3000) в регистр r1 (переменная a)
LR %r0, %r5Загрузка регистра r5 (предыдущее значение переменной a, равное 10) в регистр r0 (переменная b)
#GE00000Завершение встраивания пользовательской ассемблерной инструкции

С другой стороны, ассемблерный код, сгенерированный для модуля example07b.c, в котором используется ограничитель совпадения, показывает, что для переменной a используется один и тот же регистр r1. Обновленное значение регистра r1 после выполнения первой инструкции LR становится входным значением второй инструкции LR. Поэтому переменной b корректно присваивается обновленное значение переменной a.

Таблица 3. Ассемблерный код при использовании ограничителя совпадения (модуль example07b.c)
Ассемблерная инструкцияОбъяснение
BRASL %r14,printfвызов функции printf INITIAL …
L %r3,168(,%r15)Загрузка значения переменной c из адреса r15+168 в регистр r3: регистр r3 содержит значение 3000
L %r1,176(,%r15)Загрузка значения переменной a из адреса r15+176 в регистр r1: регистр r1содержит значение 10
#GS00000Начало встраивания пользовательской ассемблерной инструкции
LR %r1, %r3Загрузка регистра r3 (значение переменной c, равное 3000) в регистр r1 (переменная a)
LR %r0, %r1Загрузка регистра r1 (обновленное значение переменной a, равное 3000) в регистр r0 (переменная b)
#GE00000Завершение встраивания пользовательской ассемблерной инструкции

Имена регистров в списке экранирования (clobber list)

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

Рассмотрим пример из листинга 9, в котором регистр общего назначения r7 явно указан в качестве операнда ассемблерной инструкции.

Листинг 9. Модуль example09.c – использование регистра, отсутствующего в списке входных/выходных операндов
#include <stdio.h>
int main () {
   int a = 15, b = 20;
   printf ("INITIAL: a = %d, b = %d\n", a, b );
   asm ("LR   7, %1\n"
            "MSR %0,  7\n"
           :"+r"(a)
           :"r"(b)
           :"r7"
       );
   printf ("RESULT : a = %d, b = %d\n", a, b );
   return 0;
}

В строке 5 регистр r7 указан в качестве выходного операнда инструкции LR. В строке 6 инструкция MSR также использует регистр r7 в качестве входного операнда. Регистр r7, используемый в качестве операнда ассемблерных инструкций, не перечислен в списке входных и выходных операндов, поэтому его необходимо добавить в список экранирования, чтобы компилятор знал о его использовании. В общем случае, чтобы программа работала корректно, любой регистр, задействованный в ассемблерных инструкциях, должен быть включен либо в список операндов, либо в список экранирования. На основе этой информации компилятор организует распределение регистров.

Чтобы увидеть, как экранирование разных регистров влияет на производительность приложения, рассмотрим код, в котором экранируется другой регистр. В примере из листинга 10 (модуль example09a.c) вместо регистра r7 используется регистр r1.

Листинг 10. Модуль example09a.c – экранирование другого регистра
#include <stdio.h>
int main () {
   int a = 15, b = 20;
   printf ("INITIAL: a = %d, b = %d\n", a, b );
   asm ("LR   1, %1\n"
            "MSR %0,  1\n"
           :"+r"(a)
           :"r"(b)
           :"r1"
       );
   printf ("RESULT : a = %d, b = %d\n", a, b );
   return 0;
}

На рисунке 3 показаны различия в ассемблерных файлах, сгенерированных компилятором. В левой части рисунка экранируется регистр r7, а в правой – регистр r1.

Рисунок 3. Различия в коде при экранировании различных регистров
Различия в коде при экранировании различных регистров
Различия в коде при экранировании различных регистров

Из правой части рисунка 3 видно, что при экранировании регистра r1 для переменной b [ L %r3,168(,%r15) ] компилятор выбирает регистр r3. Что более важно, при экранировании регистра r1 компилятор не сохраняет его содержимое. При экранировании регистра r7 его содержимое сохраняется по адресу R15+56 [ STG %r7,56(,%r15) ]. Это означает, что добавление в список экранирования регистра r1, а не r7, избавляет от одной инструкции STORE. Рисунок 3 доказывает, что экранирование правильного регистра может повысить производительность приложения.

Если вы предпочитаете не указывать регистр самостоятельно, то можете изменить код таким образом, чтобы подходящий регистр выбирался компилятором. В следующем примере разработчик приложения добавлен временный регистровый операнд и применил ограничитель совпадения, чтобы этот операнд использовался как в качестве входного, так и в качестве выходного операндов. Этот код представлен в листинге 11 (модуль example09b.c).

Листинг 11. Модификация кода, позволяющая компилятору самостоятельно выбирать регистр
#include <stdio.h>
int main () {
   int a = 15, b = 20, tmp = 1;
   printf ("INITIAL: a = %d, b = %d\n", a, b );
   asm ("LR  %1, %2\n"
             "MSR %0, %3\n"
            :"+r"(a), "=r"(tmp)
            :"r"(b) , "1"(tmp)
           );
   printf ("RESULT : a = %d, b = %d\n", a, b );
   return 0;
}

Заключение

Встроенный ассемблер предоставляет широкие возможности встраивания ассемблерных инструкций в код C/C++, позволяя опытным разработчикам ускорять работу приложений. Компиляторы IBM XL выполняют сложные задачи по оптимизации кода на различных уровнях. По этой причине использование встроенного ассемблера для повышения производительности приложений требует глубоких знаний о методах выполнения конечного кода. Для достижения этой цели требуются тщательный анализ производительности, всестороннее планирование и тестирование.

Благодарности

Автор выражает благодарность Висду Вокшури (Visda Vokhshoori) и Нья-Ви Трану (Nha-Vy Tran) за помощь в работе над этой статьей.

Ресурсы

Ссылки


Ресурсы для скачивания


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=1028560
ArticleTitle=Расширенные возможности встроенного ассемблера для Linux на платформе z Systems
publish-date=03182016