Что делает команда g dd 345d
Перейти к содержимому

Что делает команда g dd 345d

  • автор:

Программирование на языке ассемблера

предваряться нулём, иначе компилятор не сможет отличить число от идентификатора. Примеры чисел см. в разделе 2.6 . 2.3. Символьные данные Символы и строки в языке ассемблера могут заключаться в апострофы или двойные кавычки. Если в качестве символа или внутри строки надо указать апостроф или кавычку, то делается это следующим образом: если символ или строка заключены в апострофы, то апостроф надо удваивать, а кавычку удваивать не надо, и наоборот, если символ или строка заключены в двойные кавычки, то надо удваивать кавычку и не надо удваивать апостроф. Все следующие примеры корректны и эквивалентны: ‘don»t’ , ‘don»t’ , «don’t» , «don»»t» . 2.4. Комментарии Комментарии в языке ассемблера начинаются с символа «точка с запятой» и могут начинаться как в начале строки, так и после команды. 2.5. Директива эквивалентности Директива эквивалентности позволяет описывать константы: <имя>EQU Все вхождения имени заменяются операндом . Операндом может быть константное выражение, строка, другое имя. 2.6. Директивы определения данных Языки высокого уровня обычно являются типизированными. Каждая переменная имеет тип, который накладывает ограничения на операции над переменной и на использование в одном выражении переменных разных типов. Кроме того, языки высокого уровня позволяют работать со сложными типами, таким как указатели, записи/структуры, классы, массивы, строки, множества и т.п. Язык Паскаль имеет достаточно жёсткую структуру типов. Присваивания между переменными разных типов минимальны, над указателями определены только операции присваивания, взятия значения и получение адреса. Поддерживается много сложных типов. Язык С, который создавался как высокоуровневая замена языку ассемблера, имеет гораздо менее жёсткую структуру типов. Все целочисленные типы совместимы, тип char , конечно, хранит символы, но также сопоставим с целыми типами, логический тип отсутствует в

принципе (для языка С это именно так!), над указателями определены операции сложения и вычитания. Сложные типы, такие как массивы, строки и множества, не поддерживаются. Что касается языка ассемблера, то тут вообще вряд ли можно говорить о какой-либо структуре типов. Команды языка ассемблера оперируют объектами, существующими в оперативной памяти, т.е. байтом и его производными (слово, двойное слово и т.д.). Символьный, логический тип? Какая глупость! Указатели? Вот тебе 4 байта и делай с ними, что хочешь. В итоге, конечно, и можно сделать, что хочешь, только предварительно стоит хорошо подумать, что из этого получится. Соответственно, в языке ассемблера существует 5 (!) директив для определения данных: DB ( define byte ) – определяет переменную размером в 1 байт; DW ( define word ) – определяет переменную размеров в 2 байта (слово); DD ( define double word ) – определяет переменную размером в 4 байта (двойное слово); DQ ( define quad word ) – определяет переменную размером в 8 байт (учетверённое слово); DT ( define ten bytes ) – определяет переменную размером в 10 байт. Все директивы могут быть использованы как для объявления простых переменных, так и для объявления массивов. Хотя для определения строк, в принципе, можно использовать любую директиву, в связи с особенностями хранения данных в оперативной памяти лучше использовать директиву DB . Синтаксис директив определения данных следующий: <имя>DB [, ] <имя>DW [, ] <имя>DD [, ] <имя>DQ [, ] <имя>DT [, ] Операнд задаёт начальное значение переменной. В качестве операнда может использоваться число, символ или знак вопроса, с помощью которого определяются неинициализированные переменные. Если в качестве операнда указывается строка или если указано несколько операндов через запятую, то память отводится под несколько

переменных указанного типа, т.е. получается массив. При этом именованным оказывается только первый элемент, а доступ к остальным элементам массива осуществляется с помощью выражения <имя>+ . Для того чтобы не указывать несколько раз одно и то же значение, при инициализации массивов можно использовать конструкцию повторения DUP . a db 10011001b ; Определяем переменную размером 1 байт с начальным значением, заданным в двоичной системе счисления b db ‘!’ ; Определяем переменную в 1 байт, инициализируемую символом ‘!’ d db ‘string’,13,10 ; Определяем массив из 8 байт e db ‘string’,0 ; Определяем строку из 7 байт, заканчивающую нулём f dw 1235o ; Определяем переменную размером 2 байта с начальным значением, заданным в восьмеричной системе счисления g dd -345d ; Определяем переменную размером 4 байта с начальным значением, заданным в десятичной системе счисления h dd 0f1ah ; Определяем переменную размером 4 байта с начальным значением, заданным в шестнадцатеричной системе счисления

i dd ? ; Определяем неинициализированную переменную
размером 4 байта
j dd 100 dup (0) ; Определяем массив из 100 двойных слов,
инициализированных 0
k dq 10 dup (0, 1, 2) ; Определяем массив из 30 учетверённых
слов, инициализированный повторяющимися значениями 0, 1 и 2
l dd 100 dup (?) ; Определяем массив из 100
неинициализированных двойных слов

К переменным можно применить две операции – offset и type . Первая определяет адрес переменной, а вторая – размер переменной. Однако размер переменной определяется по директиве, и даже если с директивой, например, DD определён массив из нескольких элементов, размер всё равно будет равен 4.
2.7. Команды

Команды языка ассемблера – это символьная форма записи машинных команд. Команды имеют следующий синтаксис: [<метка>:] [] [;] Метка – это имя. Метка обязательно должна отделяться двоеточием, но может размещаться отдельно, в строке, предшествующей остальной части команды. Метки нужны для ссылок на команды из других мест, например, в командах перехода. Компилятор языка ассемблера заменяет метки адресами команд. Мнемокод – это служебное слово, указывающее операцию, которая должна быть выполнена. Язык ассемблера использует не цифровые коды операций, а мнемокоды, которые легче запоминаются. Мнемокод является обязательной частью команды. Операнды команды, если они есть, отделяются друг от друга запятыми. 2.8. Операнды команд В качестве операндов команд языка ассемблера могут использоваться: регистры, обращение к которым осуществляется по именам; непосредственные операнды – константы, записываемые непосредственно в команде; ячейки памяти – в команде записывается адрес нужной ячейки. Для задания адреса существуют следующие возможности. Имя переменной , по сути, является адресом этой переменной. Встретив имя переменной в операндах команды, компилятор понимает, что нужно обратиться к оперативной памяти по определённому адресу. Обычно адрес в команде указывается в квадратных скобках, но имя переменной является исключением и может быть указано как в квадратных скобках, так и без них. Например, для обращения к переменной x в команде можно указать x или [x] . Если переменная была объявлена как массив, то к элементу массива можно обратиться, указав имя и смещение . Для этого существует ряд синтаксических форм, например: [] и [ + ] (см. раздел 5 ). Однако следует понимать, что смещение – это вовсе не индекс

элемента массива. Индекс элемента массива – это его номер, и этот номер не зависит от размера самого элемента. Смещение же задаётся в байтах, и при задании смещения программист сам должен учитывать размер элемента массива. Адрес ячейки памяти может храниться в регистре. Для обращения к памяти по адресу, хранящемуся в регистре, в команде указывается имя регистра в квадратных скобках, например: [ebx] . Как уже говорилось, в качестве регистров базы рекомендуется использовать регистры EBX, ESI, EDI и EBP. Адрес может быть вычислен по определённой формуле. Для этого в квадратных скобках можно указывать достаточно сложные выражения, например, [ebx + ecx] или [ebx + 4 * ecx] . В описаниях команд языка ассемблера для обозначения возможных операндов используют сокращения, состоящие из буквы r (для регистров), m (для памяти) или i (для непосредственного операнда) и числа 8, 16 или 32, указывающего размер операнда. Например:

add r8/r16/r32, r8/r16/r32 ; Сложение регистра с регистром
add r8/r16/r32, m8/m16/m32 ; Сложение регистра с ячейкой
памяти
add r8/r16/r32, i8/i16/i32 ; Сложение регистра с
непосредственным операндом
add m8/m16/m32, r8/r16/r32 ; Сложение ячейки памяти с
регистром
add m8/m16/m32, i8/i16/i32 ; Сложение ячейки памяти с
непосредственным операндом

Команды языка ассемблера обычно имеют 1 или 2 операнда, или не имеют операндов вообще. Во многих, хотя не во всех, случаях операнды (если их два) должны иметь одинаковый размер. Команды языка ассемблера обычно не работают с двумя ячейками памяти. 3. Пересылка и арифметические команды 3.1. Команды пересылки и обмена Одна из основных команд языка ассемблер – это команда пересылки . С её помощью можно записать в регистр значение другого регистра, константу или значение ячейки памяти, а также можно записать в ячейку памяти значение регистра или константу. Команда имеет следующий синтаксис:
MOV <операнд 1 >, <операнд 2 >По команде MOV значение второго операнда записывается в первый операнд. Операнды должны иметь одинаковый размер. Команда не меняет флаги.

mov eax, ebx ; Пересылаем значение регистра EBX в регистр
EAX
mov eax, 0ffffh ; Записываем в регистр EAX шестнадцатеричное
значение ffff
mov x, 0 ; Записываем в переменную x значение 0
mov eax, x ; Переслать значение из одной ячейки памяти в
другую нельзя.
mov y, eax ; Но можно использовать две команды MOV .

На самом деле процессор имеет много команд пересылки – код команды зависит от того, куда и откуда пересылаются данные. Но компилятор языка ассемблера сам выбирает нужный код в зависимости от операндов, так что, с точки зрения программиста, команда пересылки только одна. Для перестановки двух величин используется команда обмена : XCHG <операнд 1 >, <операнд 2 >Каждый из операндов может быть регистром или ячейкой памяти. Однако переставить содержимое двух регистров можно, а двух ячеек памяти – нет. Операнды должны иметь одинаковый размер. Команда не меняет флаги. 3.2. Оператор указания типа Как было сказано, операнды команды MOV должны иметь одинаковый размер. В некоторых случаях компилятор может определить размер операнда. Например, регистр EAX имеет размер 32 бита, а регистр DX – 16 бит. Размер переменной определяется по директиве, указанной в её объявлении. Если можно определить размер только одного операнда, то размер второго операнда подгоняется под размер первого, если это возможно. Если же можно определить размеры обоих операндов, то они должны совпадать. x db ?

mov x, 0 ; 0 может иметь любой размер, в данном случае
берётся 1 байт
mov eax, 0 ; 0 может иметь любой размер, в данном случае
берётся 4 байта
mov al, 1000h ; Ошибка – попытка записать 2-байтное
число в 1-байтный регистр
mov eax, cx ; Ошибка – размеры операндов не совпадают

Однако не всегда бывает возможно определить размер пересылаемой величины по операндам команды MOV . Например, если один из операндов является ячейкой памяти, адрес которой записан в регистре, то по этому адресу можно записать и 1 байт, и 2 байта, и 4 байта. Если второй операнд является регистром, то размер пересылаемых данных определяется по размеру регистра. Если же второй операнд является константой, то размер пересылаемых данных определить нельзя, и компилятор фиксирует ошибку. Для того чтобы избежать этой ошибки, надо явно указать размер пересылаемых данных. Для этого используется оператор PTR : PTR В качестве типа используется BYTE , WORD или DWORD .

mov [ebx], 0 ; Ошибка, т.к. 0 может иметь любой
размер
mov byte ptr [ebx], 0 ; Пересылаем 1 байт
mov dword ptr [ebx], 0 ; Пересылаем 4 байта
3.3. Команды сложения и вычитания
Команды сложения и вычитания реализуют хорошо всем

известные арифметические операции. Единственное, что нужно учитывать при использовании этих команд – особенности сложения и вычитания, связанные с представлением чисел в памяти компьютера. ADD , SUB , Команда ADD складывает операнды и записывает их сумму на место первого операнда. Команда SUB вычитает из первого операнда второй и записывает полученную разность на место первого операнда. Операнды должны иметь одинаковый размер. Если первый операнд – регистр, то второй может быть также регистром, ячейкой памяти и
непосредственным операндом. Если первый операнд – ячейка памяти, то второй операнд может быть регистром или непосредственным операндом. Возможно сложение и вычитание как знаковых, так и беззнаковых чисел любого размера. Команды меняют флаги AF, CF, OF, PF, SF и ZF. a dd 45d b dd -32d c dd ?

mov eax, a
add eax, b
mov c, eax ; c = a + b

Команды инкремента и декремента увеличивают и уменьшают на 1 свой операнд. INC DEC Операндом может быть регистр или ячейка памяти любого размера. Команды меняют флаги AF, OF, PF, SF и ZF. Команды инкремента и декремента выгодны тем, что они занимают меньше места, чем соответствующие команды сложения и вычитания. inc eax К арифметическим операциям можно также отнести команду изменения знака : NEG Операндом может быть регистр или ячейка памяти любого размера. Команда NEG рассматривает свой операнд как число со знаком и меняет знак операнда на противоположный. Команда меняет флаги AF, CF, OF, PF, SF и ZF.

mov ax, 1
neg ax ; AX = -1 = ffffh
mov bl, -128
neg bl ; BL = -128, OF = 1

3.4. Команды умножения и деления 3.4.1. Команды умножения Сложение и вычитание знаковых и беззнаковых чисел производятся по одним и тем же алгоритмам. Поэтому нет отдельных команд сложения и вычитания для знаковых и беззнаковых чисел. А вот умножение и деление знаковых и беззнаковых чисел производятся по разным алгоритмам, поэтому существуют по две команды умножения и деления. Для беззнакового умножения используется команда MUL : MUL <операнд>Операнд, указываемый в команде, – это один из сомножителей. Он может быть регистром или ячейкой памяти, но не может быть непосредственным операндом. Местонахождение второго сомножителя и результата фиксировано, и в команде явно не указывается. Если операнд команды MUL имеет размер 1 байт, то второй сомножитель берётся из регистра AL, а результат помещается в регистр AX. Если операнд команды MUL имеет размер 2 байта, то второй сомножитель берётся из регистра AX, а результат помещается в регистровую пару DX:AX. Если операнд команды MUL имеет размер 4 байта, то второй сомножитель берётся из регистра EAX, а результат помещается в регистровую пару EDX:EAX. Команда меняет флаги CF и OF. Если произведение имеет такой же размер, что и сомножители, то оба флага сбрасываются в 0. Если же размер произведения удваивается относительно размера сомножителей, то оба флага устанавливаются в 1. x dw 256

mov ax, 105
mul x ; AX = AX * x, AX = 26880, CF = OF = 0
mov eax, 500000
mov ebx, 100000
mul ebx ; EDX:EAX = EAX * EBX, EDX:EAX = 50000000000, CF =
OF = 1


Для знакового умножения используется команда IMUL :
IMUL <операнд>IMUL <операнд>, IMUL <операнд 1 >, <операнд 2 >, IMUL <операнд 1 >, <операнд 2 >Команда знакового умножения имеет несколько вариантов. Первый соответствует команде MUL – один из сомножителей указывается в команде, второй должен находиться в регистре EAX/AX/AL, а результат помещается в регистры EDX:EAX/DX:AX/AX. Второй вариант команды IMUL позволяет указать регистр, который будет содержать один из сомножителей. В этот же регистр будет помещён результат. Второй сомножитель указывается непосредственно в команде. Третий вариант команды IMUL позволяет указать и результат, и оба сомножителя. Однако результат может быть помещён только в регистр, а второй сомножитель может быть только непосредственным операндом. Первый сомножитель может быть регистром или ячейкой памяти. Четвёртый вариант команды IMUL позволяет указать оба сомножителя. Первый должен быть регистром, а второй – регистром или ячейкой памяти. Результат помещается в регистр, являющийся первым операндом. Команда IMUL устанавливает флаги так же, как и команда MUL . Однако расширение результата в регистр EDX/DX происходит только при использовании первого варианта команды IMUL . В остальных случаях часть произведения, не помещающаяся в регистр-результат, теряется, даже если в качестве результата указан регистр EAX/AX. При умножении двух 1-байтовых чисел, произведение которых больше байта, но меньше слова, в регистре-результате получается корректное произведение.

mov eax, 5
mov ebx, -7
imul ebx ; EAX = ffffffdd, EDX = ffffffff, CF = 0
mov ebx, 3
imul ebx, 6 ; EBX = EBX * 6

Директивы определения данных

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

Язык Паскаль имеет достаточно жёсткую структуру типов. Присваивания между переменными разных типов минимальны, над указателями определены только операции присваивания, взятия значения и получение адреса. Поддерживается много сложных типов.

Язык С, который создавался как высокоуровневая замена языку ассемблера, имеет гораздо менее жёсткую структуру типов. Все целочисленные типы совместимы, тип char, конечно, хранит символы, но также сопоставим с целыми типами, логический тип отсутствует в принципе (для языка С это именно так!), над указателями определены операции сложения и вычитания. Сложные типы, такие как массивы, строки и множества, не поддерживаются.

Что касается языка ассемблера, то тут вообще вряд ли можно говорить о какой-либо структуре типов. Команды языка ассемблера оперируют объектами, существующими в оперативной памяти, т.е. байтом и его производными (слово, двойное слово и т.д.). Символьный, логический тип? Какая глупость! Указатели? Вот тебе 4 байта и делай с ними, что хочешь. В итоге, конечно, и можно сделать, что хочешь, только предварительно стоит хорошо подумать, что из этого получится.

Соответственно, в языке ассемблера существует 5 (!) директив для определения данных:

  • DB (define byte) – определяет переменную размером в 1 байт;
  • DW (define word) – определяет переменную размеров в 2 байта (слово);
  • DD (define double word) – определяет переменную размером в 4 байта (двойное слово);
  • DQ (define quad word) – определяет переменную размером в 8 байт (учетверённое слово);
  • DT (define ten bytes) – определяет переменную размером в 10 байт.

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

Синтаксис директив определения данных следующий:

Операнд задаёт начальное значение переменной. В качестве операнда может использоваться число, символ или знак вопроса, с помощью которого определяются неинициализированные переменные.

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

Для того чтобы не указывать несколько раз одно и то же значение, при инициализации массивов можно использовать конструкцию повторения DUP.

a db 10011001b; Определяем переменную размером 1 байт с начальным значением, заданным в двоичной системе счисления

b db ‘!’; Определяем переменную в 1 байт, инициализируемую символом ‘!’

d db ‘string’,13,10; Определяем массив из 8 байт

e db ‘string’,0; Определяем строку из 7 байт, заканчивающую нулём

f dw 1235o; Определяем переменную размером 2 байта с начальным значением, заданным в восьмеричной системе счисления

g dd -345d; Определяем переменную размером 4 байта с начальным значением, заданным в десятичной системе счисления

h dd 0f1ah; Определяем переменную размером 4 байта с начальным значением, заданным в шестнадцатеричной системе счисления

i dd?; Определяем неинициализированную переменную размером 4 байта

j dd 100 dup (0); Определяем массив из 100 двойных слов, инициализированных 0

k dq 10 dup (0, 1, 2); Определяем массив из 30 учетверённых слов, инициализированный повторяющимися значениями 0, 1 и 2

l dd 100 dup (?); Определяем массив из 100 неинициализированных двойных слов

К переменным можно применить две операции – offset и type. Первая определяет адрес переменной, а вторая – размер переменной. Однако размер переменной определяется по директиве, и даже если с директивой, например, DD определён массив из нескольких элементов, размер всё равно будет равен 4.

Команды

Команды языка ассемблера – это символьная форма записи машинных команд. Команды имеют следующий синтаксис:

Метка – это имя. Метка обязательно должна отделяться двоеточием, но может размещаться отдельно, в строке, предшествующей остальной части команды.

Метки нужны для ссылок на команды из других мест, например, в командах перехода. Компилятор языка ассемблера заменяет метки адресами команд.

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

Операнды команды, если они есть, отделяются друг от друга запятыми.

Операнды команд

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

  • регистры, обращение к которым осуществляется по именам;
  • непосредственные операнды – константы, записываемые непосредственно в команде;
  • ячейки памяти – в команде записывается адрес нужной ячейки.

Для задания адреса существуют следующие возможности.

  • Имя переменной, по сути, является адресом этой переменной. Встретив имя переменной в операндах команды, компилятор понимает, что нужно обратиться к оперативной памяти по определённому адресу. Обычно адрес в команде указывается в квадратных скобках, но имя переменной является исключением и может быть указано как в квадратных скобках, так и без них. Например, для обращения к переменной x в команде можно указать x или [x].
  • Если переменная была объявлена как массив, то к элементу массива можно обратиться, указав имя и смещение. Для этого существует ряд синтаксических форм, например: [] и[ + ] (см. раздел 5). Однако следует понимать, что смещение – это вовсе не индекс элемента массива. Индекс элемента массива – это его номер, и этот номер не зависит от размера самого элемента. Смещение же задаётся в байтах, и при задании смещения программист сам должен учитывать размер элемента массива.
  • Адрес ячейки памяти может храниться в регистре. Для обращения к памяти по адресу, хранящемуся в регистре, в команде указывается имя регистра в квадратных скобках, например: [ebx]. Как уже говорилось, в качестве регистров базы рекомендуется использовать регистры EBX, ESI, EDI и EBP.
  • Адрес может быть вычислен по определённой формуле. Для этого в квадратных скобках можно указывать достаточно сложные выражения, например, [ebx + ecx] или [ebx + 4 * ecx].

В описаниях команд языка ассемблера для обозначения возможных операндов используют сокращения, состоящие из буквы r (для регистров), m (для памяти) или i (для непосредственного операнда) и числа 8, 16 или 32, указывающего размер операнда. Например:

add r8/r16/r32, r8/r16/r32; Сложение регистра с регистром

add r8/r16/r32, m8/m16/m32; Сложение регистра с ячейкой памяти

add r8/r16/r32, i8/i16/i32; Сложение регистра с непосредственным операндом

add m8/m16/m32, r8/r16/r32; Сложение ячейки памяти с регистром

add m8/m16/m32, i8/i16/i32; Сложение ячейки памяти с непосредственным операндом

Команды языка ассемблера обычно имеют 1 или 2 операнда, или не имеют операндов вообще. Во многих, хотя не во всех, случаях операнды (если их два) должны иметь одинаковый размер. Команды языка ассемблера обычно не работают с двумя ячейками памяти.

Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:

Сравниваем производительность C# и C++ в задачах обработки изображений

Бытует мнение, что C# не место в вычислительных задачах, и мнение это вполне обоснованное: JIT-компилятор вынужден компилировать и оптимизировать код на лету в процессе выполнения программы с минимальными задержками, у него попросту нет возможности потратить больше вычислительных ресурсов, чтобы сгенерить более эффективный код, в отличие от компилятора C++, которые может потратить на это дело минуты и даже часы.

Однако, в последние годы эффективность JIT-компилятора заметно возросла, да и в сам фреймворк завезли ряд полезных фишек, например, интринсики.

И вот стало мне интересно: а можно ли в 2020 году, используя .NET 5.0, написать код, который бы не сильно уступал по производительности C++? Оказалось, что можно.

Мотивация

Я занимаюсь разработкой алгоритмов обработки изображений, причём на достаточно низком уровне. То есть это не жонглирование кирпичиками в Python, а именно разработка чего-то нового и, желательно, производительного. Код на Python работает непозволительно долго, тогда как использование C++ приводит к снижению скорости разработки. Оптимальный баланс между продуктивностью и производительностью для подобных задач достигается при использовании C# и Java. В подтверждение моих слов — проект Fiji.

Раньше для прототипирования я использовал C#, а готовые алгоритмы, которым критична производительность, переписывал на C++, пихал в либу и дёргал либу из C#. Но в этом случае страдала переносимость, да и отлаживать код было не очень удобно.

Но это было давно, с тех пор .NET шагнул далеко вперёд, и мне стало интересно, могу ли я отказаться от нативной библиотеки на C++ и перейти полностью на C#?

Сценарий

Сравнивать же языки я буду на примере базовых методов обработки изображений: сумма изображений, поворот, свёртка, медианная фильтрация. Именно подобные методы чаще всего приходится писать на C++. Особенно критично время работы свёртки.

Для каждого из методов, кроме медианной фильтрации, было сделано по три реализации на C# и C++:

  • Наивная реализация с использованием методов типа GetPixel(x, y) и SetPixel(x, y, value);
  • Оптимизированная реализация с использованием указателей и работы с ними на низком уровне;
  • Реализация с использованием интринсков (AVX).

В случае медианной фильтрации использовались библиотечные функции (Array.Sort, std::sort), поэтому это было, фактически, сравнение реализаций этих функции, а не пользовательского кода. В перспективе имеет смысл подумать об использовании сортировочных сетей.

Также, чтобы уравнять языки в возможностях, я в C# использовал unmanaged память и обращался к пикселям без каких-либо проверок на выход за границы. А то как-то нечестно получается, что C++ использует UB для достижения высокой производительности, а C# — нет.

Реализация методов выложена на Github, смысла постить сюда портянки кода я не вижу, просто приведу пример кода на C#:

[MethodImpl(MethodImplOptions.AggressiveOptimization)] public static void Sum_ThisProperty(NativeImage img1, NativeImage img2, NativeImage res) < for (var j = 0; j < res.Height; j++) for (var i = 0; i < res.Width; i++) res[i, j] = img1[i, j] + img2[i, j]; >[MethodImpl(MethodImplOptions.AggressiveOptimization)] public static void Sum_Optimized(NativeImage img1, NativeImage img2, NativeImage res) < var w = res.Width; for (var j = 0; j < res.Height; j++) < var p1 = img1.PixelAddr(0, j); var p2 = img2.PixelAddr(0, j); var r = res.PixelAddr(0, j); for (var i = 0; i < w; i++) r[i] = p1[i] + p2[i]; >> [MethodImpl(MethodImplOptions.AggressiveOptimization)] public static void Sum_Avx(NativeImage img1, NativeImage img2, NativeImage res) < var w8 = res.Width / 8 * 8; for (var j = 0; j < res.Height; j++) < var p1 = img1.PixelAddr(0, j); var p2 = img2.PixelAddr(0, j); var r = res.PixelAddr(0, j);доступна for (var i = 0; i < w8; i += 8) < Avx.StoreAligned(r, Avx.Add(Avx.LoadAlignedVector256(p1), Avx.LoadAlignedVector256(p2))); p1 += 8; p2 += 8; r += 8; >for (var i = w8; i < res.Width; i++) *r++ = *p1++ + *p2++; >> 

Результаты

Перейдём к результатам. В ячейках таблицы указано время работы (1/10 перцентиль) тестируемых методов в микросекундах для изображений размером 256×256 в градациях серого с типом пикселя float 32 bit.

dotnet build -c Release

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *