Содержание


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

Вводное руководство по ассемблерным инструкциям, встраиваемым в код приложений для мэйнфреймов, разрабатываемых на C/C++

Comments

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

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

Регистры

В ассемблере используются 16 64-разрядных регистров общего назначения с номерами от 0 до 15. Все регистры общего назначения, за исключением регистра 0,можно использовать для хранения базовых адресов или индексов при выполнении арифметических операций. В основных арифметических и логических операциях регистры общего назначения можно использовать в качестве аккумуляторов. Назначение некоторых регистров общего назначения платформы z Systems под управлением ОС Linux описано в таблице 1.

Таблица 1. Назначение регистров платформы z Systems под управлением Linux
Имя регистраНазначение
r2, r3 Параметры, возвращаемые значения
r4, r5, r6 Параметры
r13 Базовый регистр пула литералов
r14 Обратный адрес
r15 Указатели стека

Регистры общего назначения с номерами 0-15 имеют соответствующие имена – от r0 до r15. Некоторые инструкции (например, MR – умножение и DR – деление) требуют использования в операнде двух смежных регистров. При выполнении таких операций необходимо выделять четно-нечетную пару смежных регистров общего назначения. Для обращения к операнду регистровой пары используется регистр с четным номером. На рисунке 1 показано, как регистры общего назначения выделяются по отдельности и парами.

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

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

Код условия

Код условия (Condition Code, cc) – это 18-й и 19-й биты слова состояния программы (PSW) процессора. Будучи двухбитовым значением, код условия может принимать значения от 0 до 3 в зависимости от результатов выполнения определенных инструкций. Когда коду условия присваивается какое-либо значение, оно хранится в нем до тех пор, пока не будет изменено другой инструкцией. Большинство арифметических и логических операций, а также некоторые другие инструкции, изменяют значение кода условия. Значение кода условия может влиять на принятие инструкциями ветвления решения о передаче управления.

В примере из листинга 1 демонстрируется использование кода условия на высоком уровне. Более подробно формат инструкций и операторов встроенного ассемблера будет рассмотрен в следующей статье этой серии.

Листинг 1. Инструкция ветвления и код условия
1  #include <stdio.h>
2 
3  void myAdd(int a, int b) {
4    int noOverflow = 1;
5    asm("AR  %0, %2\n"                 // При переполнении инструкция AR поместит в код условия значение 3
6        "BRC 0xE, OK\n"                  // Инструкция BRC проверяет код условия: если он не равен 3 (переполнения не было), выполняется переход к строке 8, т. е. строка 7 пропускается

7        "XR  %1, %1\n"                   // Инструкция XR сбрасывает значение переменной noOverflow
8        "OK:\n"
9        : "+r"(a), "+r"(noOverflow)
10       : "r"(b)
11       : "cc"                         // Код условия помещен в список экранирования, чтобы
12      );                                // сообщить компилятору о том, что он может быть изменен
13   if (noOverflow){
14      printf ("Sum is %d\n", a);
15   } else {
16      printf ("Overflow\n");
17   }
18 }
19 int main() {
20   int a, b;
21   a = 11, b = -2;                       // Значения a=11, b= -2 не приводят к переполнению,
22   myAdd(a, b);                         // в этом случае выводится сообщение "Sum is 9".
23   a = 0x7fffffff, b = 0x7fffffff;     // Присваиваем переменным a и b большие значения, чтобы возникло переполнение.
24   myAdd(a, b);                        // Возникнет переполнение и выводится сообщение "Overflow".
25   return 0;
26 }

При помощи параметров %0, %1 и %2 мы обращаемся к первому (переменная a), второму (переменная noOverflow) и третьему операндам (переменная b), указанным в строках 9 и 10.

Инструкция ADD (AR) в строке 5 устанавливает значение кода условия следующим образом:

0Результат сложения равен нулю, переполнения нет.
1Результат меньше нуля, переполнения нет.
2Результат больше нуля, переполнения нет.
3Возникло переполнение.

В строке 6 инструкция условного ветвления BRC проверяет значение кода условия и определяет дальнейший путь выполнения программы. Если код условия содержит значение 0, 1 или 2 (т. е. переполнения не возникло), то выполняется переход по метке "OK" на строку 8, а инструкция исключающего ИЛИ (XR) в строке 7 пропускается. Если же код условия содержит значение 3 (признак переполнения), то выполняется следующая инструкция (XR) в строке 7, которая сбрасывает значение переменной noOverflow.

Если переполнения не возникло, то код из листинга 1 выводит на экран сумму переменных, в противном случае выводится сообщение "Overflow".

Листинг 2. Компиляция и запуск кода из листинга 1
$ xlc -o ex1 ./example01.c
$ ./ex1
Sum is 9                            // Результат сложения 11 и – 2
Overflow                            // В результате сложения переменных a = 0x7fffffff и b = 0x7fffffff возникло переполнение

Полная информация о всех значениях кода условий, которые могут быть установлены различными инструкциями, приведена в документации к вашей системе. В частности, эта информация содержится в приложении Appendix C. Condition-Code Settings книги z/Architecture Principles of Operation (EN) (документ IBM SA22-7832-10).

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

Основные инструкции работают с данными, хранящимися в регистрах общего назначения. Другие инструкции работают с данными слова состояния программы (это могут быть данные часов истинного времени или данные, поступающие из потока команд). Имена некоторых инструкций включают в себя буквы, обозначающие разрядность операндов, например, H (halfword – полуслово) для 16-разрядных, F (word – слово) – для 32-разрядных и G (doubleword – двойное слово) – для 64-разрядных операндов. Если инструкция работает с 32-разрядными операндами, то она может содержать (не обязательно) в своем имени букву F. Если инструкция с тем же именем работает с 64-разрядными операндами, то ее имя будет содержать букву G. Инструкция с тем же именем, работающая с 64-разрядным первым и 32-разрядным вторым операндами, будет содержать в имени буквы GF. За полной информацией обо всех инструкциях обращайтесь к документации вашей системы.

Для упрощения рассмотрения материала все ассемблерные инструкции в этой статье разделены на несколько обобщенных групп. Мы рассмотрим только некоторые основные инструкции, используемые разработчиками чаще всего. Ссылка на полный список ассемблерных инструкций приведена в разделе Ресурсы.

Группа алгебраических арифметических инструкций

В эту группу входят инструкции, выполняющие алгебраические действия с операндами. Операнды арифметических операций могут представлять собой единственный регистр, адрес памяти или непосредственное значение. В случаях с двумя операндами операция применяется к обоим из них, но результат сохраняется в первом операнде. Соответственно, над первым операндом выполняются как операции чтения (READ), так и операции записи (WRITE).

Для многих алгебраических арифметических инструкций код условия принимает следующие значения:

0Результат сложения равен нулю, переполнения нет.
1Результат меньше нуля, переполнения нет.
2Результат больше нуля, переполнения нет.
3Возникло переполнение.

Перейдем к рассмотрению наиболее часто используемых форматов.

OP R1, R2: формат регистр-регистр
Значение, хранящееся в регистре, указанном с помощью второго операнда (R2), используется в таких операциях как сложение, вычитание, умножение или деление, вместе со значением, хранящимся в регистре, указанном с помощью первого операнда (R1). Результат операции сохраняется в первом операнде R1.
R1 ← R1 OP R2

OP R1, I2: формат регистр-значение
Непосредственное значение, указанное с помощью второго операнда (I2), используется в операциях вместе со значением, хранящимся в регистре, указанном с помощью первого операнда (R1). Результат операции сохраняется в первом операнде R1.
R1 ← R1 OP I2

OP R1, D2 (X2,B2): формат регистр-адрес
Значение, хранящееся по адресу, указанному с помощью второго операнда, используется в операциях вместе со значением, хранящимся в регистре, указанном с помощью первого операнда (R1). Результат операции сохраняется в первом операнде R1.
R1 ← R1 OP [значение-хранящееся-по-фактическому-адресу D2 (X2,B2)]

Фактический адрес во втором операнде вычисляется следующим образом:

   D2: смещение относительно базового адреса 
+  B2: базовый адрес
+  X2: указатель на базовый адрес

Примеры алгебраических арифметических инструкций
Ассемблерные инструкции могут содержаться либо в приложениях, написанных непосредственно на ассемблере, либо в приложениях, написанных на языке C/C++ с использованием встроенного ассемблера. В этом разделе мы покажем, как поместить алгебраическую арифметическую инструкцию AH (Add Halfword) в код приложения, разрабатываемого на языке C. Инструкция AH алгебраически складывает значение двухбайтового поля (полуслова), хранящегося по адресу из второго операнда, со значением, хранящимся в регистре, указанном с помощью первого операнда. Результат помещается в регистр первого операнда. В листинге 3 приведен фрагмент кода на языке C, предшествующий инструкции asm.

Листинг 3. Модуль example02.c – исходный код на языке C без ассемблерных инструкций
1   #include <stdio.h>
2   int main() {
3       int a = 0;
4       int ar[] = {0x00112233, 0x44556677};
5       printf ("a = 0x%08x, ar[0] = 0x%08x, ar[1] = 0x%08x\n", a, ar[0], ar[1]);
6       return 0;
7   }

При запуске модуля example02.c на экран выводятся значения операндов a, ar[0] и ar[1], как продемонстрировано в листинге 4.

Листинг 4. Компиляция и запуск модуля example02.c
$ xlc -o ex2 ./example02.c
$ ./ex2
a = 0x00000000, ar[0] = 0x00112233, ar[1] = 0x44556677

Цель данного примера заключается в добавлении в код инструкции AH, которая суммирует двухбайтовое значение, хранящееся по адресу из регистра ar, и значение переменной a. Двухбайтовое значение, хранящееся по адресу в ar – это 0x0011, а исходное значение переменной a – 0x0, следовательно, итоговым значением переменной a после выполнения операции будет 0x11.

(1) Добавление инструкции AH в ассемблерный код вручную
Файл с ассемблерным кодом example02.s получен в результате компиляции модуля example02.c с опцией –S.
$ xlc example02.c –c –S

Наиболее важная часть ассемблерного кода из файла example02.s представлена в листинге 5.

Листинг 5. Исходный ассемблерный код модуля example02.s без дополнительных инструкций
  .L_main:
1         STMG    %r13,%r15,104(%r15)
2         LARL    %r13,$CONSTANT_AREA
3         LAY     %r15,-184(,%r15)     # Резервируем 184 байта в стеке; r15 содержит вершину стека
4         MVHI    176(%r15),0          # Помещаем 0 (значение переменной a) в байты 176 → 179 регистра r15
5         MVHHI   168(%r15),17         #  Байты 168 →169 регистра r15 содержат значения 0x00 и 0x11 (т. е. 1710)
6         MVHHI   170(%r15),8755       # Байты 170 →171 регистра r15 содержат значения 0x22 и 0x33 (т. е. 875510)
7         MVHHI   172(%r15),17493      # Байты 172 →173 регистра r15 содержат значения 0x44 и 0x55 (т. е. 1749310)
8         MVHHI   174(%r15),26231      # Байты 174 →175 регистра r15 содержат значения 0x66 и 0x77 (т. е. 2623110)
          LA      %r2,16(,%r13)        # Загрузка параметров для функции printf
          LGF     %r3,176(,%r15)
          LGF     %r4,168(,%r15)
          LGF     %r5,172(,%r15)
          BRASL   %r14,printf          # Вызов функции printf

В строках 4-8 выполняется заполнение стека значениями операндов a (содержит 0), ar[0] (содержит 0x00112233) и ar[1] (содержит 0x44556677).

На рисунке 2 показано содержимое стека после выполнения команды из строки 8. Начиная со строки 9 выполняется подготовка параметров, передаваемых в функцию printf, которая затем вызывается с помощью инструкции BRASL.

Рисунок 2. Содержимое стека после выполнения команды из строки 8
Содержимое стека после выполнения команды из строки 8
Содержимое стека после выполнения команды из строки 8

Чтобы добиться требуемых результатов, в файл example02.s необходимо добавить инструкцию AH (в формате регистр-адрес), поместив ее перед блоком подготовки параметров функции printf, как показано в листинге 6.

Листинг 6. Добавление ассемблерных инструкций в код модуля example02.s
   .L_main:
1          STMG    %r13,%r15,104(%r15)
2          LARL    %r13,$CONSTANT_AREA
3          LAY     %r15,-184(,%r15)      # Резервируем 184 байта в стеке; r15 содержит вершину стека
4          MVHI    176(%r15),0           # Помещаем 0 (значение переменной a) в байты 176 → 179 регистра r15
5          MVHHI   168(%r15),17          # Байты 168 →169 регистра r15 содержат значения 0x00 и 0x11 (т. е. 1710)
6          MVHHI   170(%r15),8755        # Байты 168 →169 регистра r15 содержат значения 0x00 и 0x11 (т. е. 875510)
7          MVHHI   172(%r15),17493       # Байты 172 →173 регистра r15 содержат значения 0x44 и 0x55 (т. е. 1749310)
8          MVHHI   174(%r15),26231       # Байты 174 →175 регистра r15 содержат значения 0x66 и 0x77 (т. е. 2623110)

9          LA      %r1,168(,%r15)        # Загружаем адрес массива ar в стеке в регистр r1
10         L       %r0,176(,%r15)        # Загружаем значение переменной a (байты 176 → 179 регистра r15) в регистр r0
11         AH      %r0, 0(%r1)           # Получаем двухбайтовое значение со сдвигом 0 относительно адреса массива ar (регистр r1) и помещаем результат в регистр r0 
12         ST      %r0,176(,%r15)        # Результат выполнения инструкции AH загружается из регистра r0 обратно в переменную a (байты 176 → 179 регистра r15)

           LA      %r2,16(,%r13)         # Загрузка параметров для функции printf
           LGF     %r3,176(,%r15)
           LGF     %r4,168(,%r15)
           LGF     %r5,172(,%r15)
           BRASL   %r14,printf           # Вызов функции printf

Инструкция AH должна знать адрес массива ar, чтобы прочесть два байта из этого местоположения. Также ей требуется знать текущее значение переменной a, чтобы выполнить операцию сложения. Чтобы подготовить требуемую информацию для инструкции AH, были добавлены строки 9 и 10.

  • Строка 9: инструкция LA загружает адрес массива ar в регистр r1.
  • Строка 10: инструкция L загружает текущее значение переменной a в регистр r0.
  • Строка 11: регистры r0 и r1 используются инструкцией AH.

В строке 11 инструкция AH выполняет следующие операции:

  1. Получение двухбайтового значения, хранящегося со сдвигом 0 относительно адреса массива ar.
    Поскольку ar ={0x00112233, 0x44556677}, два байта со сдвигом 0 представляют собой значение 0x0011.
  2. Добавление полученного двухбайтового значения к содержимому регистра r0.
    Поскольку на текущий момент регистр r0 содержит значение переменной a, равное 0, то суммарное значение будет 0x11.
  3. Сохранение результатов в регистре r0: теперь регистр r0 содержит новое значение, 0x11.

После выполнения инструкции AH в строке 11 регистр r0 содержит новое значение. Инструкция ST в строке 12 сохраняет результат в переменную a.

Чтобы оценить результат добавления инструкции AH и других ассемблерных инструкций (строки 9-12), скомпилируем и запустим на выполнение обновленный ассемблерный код.

Листинг 7. Компиляция и запуск обновленного ассемблерного кода
$ xlc -o ex2_new ./example02.s
$ ./ex2_new
a = 0x00000011, ar[0] = 0x00112233, ar[1] = 0x44556677           → a имеет значение 0x11, как и ожидалось

(2) Добавление инструкции AH в код C при помощи встроенного ассемблера
Встроенный ассемблер позволяет получить тот же самый результат с существенно меньшими усилиями.

Листинг 8. Модуль example02.c – добавление оператора встроенного ассемблера в код C
1 #include <stdio.h>
2 int main() {
3     int a = 0;
4     int ar[] = {0x00112233, 0x44556677};
5     asm( "AH %0, %1" : "+r"(a) : "m"(ar[0]) );                                 // здесь добавлена инструкция AH  
6     printf ("a = 0x%08x, ar[0] = 0x%08x, ar[1] = 0x%08x\n", a, ar[0], ar[1]);
7     return 0;
8 }

Все, что необходимо сделать при использовании встроенного ассемблера – это добавить в код основную инструкцию (в нашем случае это инструкция AH). Обо всех вспомогательных операциях (например, LOAD ADDRESS, LOAD и STORE в строках 9, 10 и 12) позаботится компилятор. Использование встроенного ассемблера в коде C должно привести к тем же самым результатам, что и в предыдущем примере.

Листинг 9. Компиляция и запуск обновленного кода C с оператором встроенного ассемблера
$ xlc -o ex2_asm ./example02_asm.c
$ ./ex2_asm
a = 0x00000011, ar[0] = 0x00112233, ar[1] = 0x44556677          → a имеет значение 0x11, как и ожидалось

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

Группа инструкций сравнения

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

0Два операнда равны
1Первый операнд меньше второго
2Первый операнд больше второго
3Неприменимо

COMP R1, R2: формат регистр-регистр
Значение, хранящееся в регистре, указанном с помощью второго операнда (R2), сравнивается со значением, хранящимся в регистре, указанном с помощью первого операнда (R1).

COMP R1, I2: формат регистр-значение
Непосредственное значение, указанное с помощью второго операнда (2), сравнивается со значением, хранящимся в регистре, указанном с помощью первого операнда (R1).

COMP R1, D2 (X2,B2): формат регистр-адрес
Значение, хранящееся по адресу, указанному с помощью второго операнда, сравнивается со значением, хранящимся в регистре, указанном с помощью первого операнда (R1).

COMP D1(B1), I2: формат адрес-значение
Непосредственное значение, указанное с помощью второго операнда (I2), сравнивается со значением, хранящимся по адресу, указанному с помощью первого операнда.

Примеры инструкций сравнения
CH 5, 0x30 (0, 9)
Инструкция CH (Compare Halfword) сравнивает 16-разрядное целое число со знаком (полуслово), хранящееся в памяти, с содержимым регистра. Инструкция CH принимает первый операнд как 32-разрядное целое число со знаком; смещение рассматривается как 12-разрядное целое число без знака.

Исходные данные этого примера:

Таблица 2. Значения регистров и ячеек памяти до вызова инструкции CH

Символ в коде операции
OP R1, D2(X2,B2)
Ассемблерный формат
CH 5, 0x30 (0, 9)
Значение Содержимое
OP
R1
D2
X2
B2
CH
5
0x30
0
9
Compare Halfword – сравнение полуслова
Регистр 5
Смещение
Индекс 0
Регистр 9

0xFFFF 9000 = – 2867210
0x30
0x0
0x00012050
Ячейка памяти 12080 – 12081 0x9000 = – 2867210

Эффективный адрес: B2 + X2 + D2 = 0x00012050 + 0x0 + 0x30 = 0x00012080.
Значение двух байтов, хранящихся в ячейке памяти 12080: 0x9000, или – 2867210.
Содержимое регистра 5: 0xFFFF 9000, или – 2867210.

Поскольку эти два значения равны, код условия содержит 0.

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

Группа инструкций условного ветвления

Инструкции условного ветвления проверяют код условия в текущем слове состояния программы. Если код условия имеет одно из заданных маской значений, то поле "адрес следующей инструкции" в слове состояния заменяется адресом перехода, указанным в инструкции ветвления. В противном случае выполняется обычная очередность команд.

Инструкции условного ветвления не изменяют значение кода условия.

BC M1, R2: формат регистр-регистр
Если код условия имеет одно из значений, определяемых маской M1, управление передается по адресу, хранящемуся в регистре, указанном с помощью второго операнда (R2).

BC M1, D2 (X2,B2): формат регистр-память
Если код условия имеет одно из значений, определяемых маской M1, управление передается по адресу, вычисляемому на основе D2, X2 и B2.

Маска
Операнд M представляет собой 4-разрядную маску. Четыре значения кода условия (0, 1, 2 и 3) сопоставляются со значениями разрядов (битов) этой маски следующим образом:

Таблица 3. Сопоставление значений кода условия и разрядов маски
2-разрядный код условия Биты маски Значение маски
002, или 010 1 0 0 0 0x8
012, или 110 0 1 0 0 0x4
102, или 210 0 0 1 0 0x2
112, или 310 0 0 0 1 0x1

Выбор соответствующего бита маски производится на основании значения кода условия. Если бит маски, выбранный исходя из кода условия, равен единице, то условный переход выполняется, в противном случае выполняется обычная очередность команд. Как следует из листинга 1, для выполнения перехода в случае, когда код условия не равен 3 (т. е. равен одному из значений {0, 1, 2}), маска должны иметь значение 11102, или 0xE или в шестнадцатеричной системе.

Если все четыре бита маски или операнд R2 равны нулю, инструкция ветвления эквивалентна пустой команде. Аналогично, если маска имеет значение 11112, выполняется безусловный переход (пока R2 не станет равен нулю). Таким образом, выполнение инструкции BCR 15, 0 может существенно снизить производительность приложения.

Примеры инструкций условного ветвления

Рассматриваемая ниже функция absoluteValue принимает в качестве аргумента целое число и возвращает его абсолютное значение. Эту функцию можно записать следующим образом:

int absoluteValue(int a) { return a < 0 ? –a : a; }

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

Листинг 10. Операторы встроенного ассемблера, содержащие инструкции сравнения и условного ветвления
1   int absoluteValue(int a) {
2        asm (" CFI %0, 0\n"            // Сравниваем значение переменной a с 0
3             " BRC 0xA, DONE\n"        // Если a >= 0, переходим к метке DONE (т. е. пропускаем строку 4)
4             " LCR %0, %0\n"           // Если попали сюда, значит, a <0, и мы применяем двоичное дополнение к переменной a, чтобы изменить ее знак
5             " DONE:\n"
6             :"+r"(a)
7             );
8       return a;
9   }

Параметр %0 ссылается на первый операнд оператора встроенного ассемблера, переменную a.

В строке 2 значение переменной a сравнивается с нулем. Согласно разделу Группа инструкций сравнения, инструкция CFI устанавливает значение кода условия в слове состояния программы следующим образом.

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

Согласно логике функции absoluteValue(int a), она возвращает значение переменной a, если оно больше или равно 0. Это равноценно установке значения кода условия в 0 или 2. Маска, соответствующая этим значениям, будет равна 1010, или 0xA в шестнадцатеричной системе.

Инструкция BRC в строке 3 проверяет, соответствует ли текущее значение кода условия маске 0xA. Если это условие выполняется, управление передается по метке DONE в строке 5, и функция готова вернуть исходное значение переменной a без каких-либо модификаций. На этом пути выполнения функция absoluteValue(int a) возвращает исходное значение переменной a при условии a >= 0.

Если инструкция BRC в строке 3 не находит совпадения, то выполняется инструкция LCR (Load Complement) в строке 4, которая загружает двоичное дополнение второго операнда (переменной a) по адресу первого операнда (переменная a), изменяя таким образом знак переменной a. На этом пути выполнения функция absoluteValue(int a) возвращает значение -a при отрицательном значении переменной a.

Для достижения максимальной производительности своих приложений разработчики ПО могут выбирать оптимальные инструкции из огромного набора ассемблерных инструкций, доступных для платформы z Systems. Например, для повышения производительности предыдущую функцию можно написать по-другому (см. листинг 12).

Группа инструкций загрузки

Инструкции загрузки (Load) помещают второй операнд (в исходном виде или дополненное знаком) на место первого операнда.
Операнды имеют следующее направление: Операнд1 Операнд2.

L R1, R2: формат регистр-регистр
Значение, хранящееся в регистре, указанном с помощью второго операнда (R2), загружается (в исходном виде или дополненное знаком) в регистр, указанный с помощью первого операнда (R1).

L R1, I2: формат регистр-значение
Непосредственное значение, указанное с помощью второго операнда (I2), загружается (в исходном виде или дополненное знаком) в регистр, указанный с помощью первого операнда (R1).

L R1, D2 (X2,B2): формат регистр-адрес
Значение, хранящееся по адресу, указанному с помощью второго операнда, загружается (в исходном виде или дополненное знаком) в регистр, указанный с помощью первого операнда (R1).

Примеры инструкций загрузки
Рассмотрим модифицированную функцию absoluteValue из раздела Примеры инструкций условного ветвления, чтобы продемонстрировать использование инструкций загрузки.

Листинг 11. Пример инструкции загрузки
1    int absoluteValue(int a) {
2        asm (" LTR %0, %0\n"           // Загрузка и проверка значения переменной a 
3             " BRC 0xA, DONE\n"        // Если a >= 0, переходим по метке DONE (т. е. пропускаем строку 4)
4             " LCR %0, %0\n"           // Если мы попали сюда, значит, a < 0, и мы применяем двоичное дополнение к переменной a, изменяя ее знак
5             " DONE:\n"
6             :"+r"(a)
7            );
8       return a;
9   }

В строке 2 инструкция LTR выполняет две операции:

  1. Загружает исходное значение второго операнда по адресу первого операнда.
  2. Обновляет значение кода условия на основании загруженного значения.
0Загруженное значение равно нулю
1Загруженное значение меньше нуля
2Загруженное значение больше нуля
3Неприменимо

Поскольку оба операнда представляют собой один и тот же регистр, хранящий значение переменной a, то использование инструкции LTR в строке 2 эквивалентно сравнению значения a с 0 без перемещения данных.

Инструкция BRC в строке 3 проверяет, соответствует ли текущее значение кода условия маске 0xA. Если это условие выполняется (т. е. a >= 0), управление передается по метке DONE в строке 5, и функция возвращает исходное значение переменной a без каких-либо модификаций. В противном случае (a < 0) выполняется инструкция LCR в строке 4, которая загружает двоичное дополнение второго операнда (переменной a), изменяя знак переменной a, и функция возвращает значение -a.

Для повышения производительности можно переписать эту же функцию с помощью других ассемблерных инструкций. В следующем примере единственная инструкция загрузки заменяет собой две предыдущих инструкции загрузки (LOAD), одну инструкцию ветвления (BRANCH) и использование метки.

Листинг 12. Использование другой ассемблерной инструкции
1   int absoluteValue(int a) {
2       asm (" LPGFR %0, %0\n" :"+r"(a) );          // Загрузка абсолютного значения переменной a
3       return a;
    }

Инструкция LPGFR загружает абсолютное значение второго операнда в первый операнд. Поскольку оба операнда представляют собой переменную a, инструкция фактически выполняет присваивание a = | a |. После выполнения инструкции LPGFR функция возвращает ожидаемое значение.

Группа инструкций сохранения

Инструкции сохранения (Store, ST) помещают первый операнд на место второго операнда.

Операция применяется к операндам в следующем направлении: Операнд1 Операнд2.

ST R1, D2 (X2,B2): формат регистр-адрес
Значение, хранящееся в регистре, указанном с помощью первого операнда (R1), загружается без изменений по адресу, указанному с помощью первого операнда

Примечание. Для платформы z Systems существует множество других инструкций. В этой статье рассматриваются только типичные инструкции.

Примеры инструкции сохранения
В следующем примере инструкция ST сохраняет значение переменной a по адресу, хранящемуся в переменной b. Поскольку перед началом выполнения инструкции a = 1 и b = 0, то после ее выполнения переменная b должна равняться 1, а функция возвратить 0.

В этом примере используется адресный ограничитель "m", существенно упрощающий вычисление адреса.

Листинг 13. Пример инструкции сохранения
1   int main() {
2       int a = 1, b = 0;               // b = 0
3       asm("ST %1,%0\n"                // Сохраняем значение переменной a по адресу из переменной b 
4          :"=m"(b)
5          :"r"(a)
6          );
7       return a==b ? 0 : 1;           // Из условия в строке 2 следует, что переменная b должна стать равной 1. Соответственно функция должна возвратить 0.

8   }

Заключение

В общем случае направление операции инструкции, работающей с двумя операндами, зависит от ее типа. Инструкции сравнения (COMPARE) и проверки (TEST) записывают результаты своей работы в код условия. Инструкции ветвления (BRANCH) проверяют значение кода условия для определения точки передачи управления. Инструкции загрузки (LOAD), работающие с двумя операндами, загружают второй операнд в первый операнд, т. е. направление операции имеет вид Операнд1Операнд2. С другой стороны, инструкции сохранения (STORE), наоборот, сохраняют первый операнд во втором операнде, т. е. направление операции имеет вид Операнд1Операнд2. Арифметические инструкции выполняют действия над обоими операндами и сохраняют результат в первом операнде, т. е. в этом случае направление операции имеет вид Операнд1Операнд1 OP Операнд2.

Несмотря на то, что Linux на платформе z Systems работает с той же архитектурой, что и платформы IBM z/OS®, z/TPF, z/VSE® и z/VM®, встроенный ассемблер для Linux на платформе z Systems позволяет работать только с ассемблерными инструкциями. Инструкции и макросы высокоуровневого ассемблера (HLASM), например DS, DD или GETMAIN, использовать нельзя.

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

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

Автор выражает благодарность Висду Вокшури (Visda Vokhshoori), руководителю направления z/OS группы по оптимизации компиляторов лаборатории Toronto Software Lab, IBM Canada, за помощь в работе над этой статьей.

Ресурсы

Ссылки


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


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=1028556
ArticleTitle=Повышение производительности приложений IBM z Systems с использованием встроенного ассемблера компилятора IBM XL C/C++
publish-date=03182016