Наши проекты:
Журнал · Discuz!ML · Wiki · DRKB · Помощь проекту |
||
ПРАВИЛА | FAQ | Помощь | Поиск | Участники | Календарь | Избранное | RSS |
[3.234.246.109] |
|
Сообщ.
#1
,
|
|
|
Есть рабочий код (Лазарус64бита):
{$ASMMODE INTEL} procedure DivMod(numerator, denominator, quotient: DWord); begin asm mov rdx, 0; mov rax, [numerator]; mov rcx, [denominator]; div rcx; mov [quotient], rax; mov [denominator], rdx; mov [numerator], rcx; end; end; Параметры процедуры здесь не объявлены как var, поэтому не передадутся во вне, но зато доступны из ассемблера напрямую. Если их объявить как var, то код перестанет работать, потому что значения надо будет достать по ссылке. Обойти это можно, создав 3 локальные переменные и работая в ассемблере через них, что на мой взгляд - избыточно. Второй вариант, доставать значение по ссылке через промежуточный регистр, что тоже не особо красиво, примерно так: mov rax, [numerator]; mov rax, [rax]; и потом, в обратную сторону. Соответственно вопрос, возможно проблема решается каким-нибдуь соглашением о вызовах, типа register, assembler, cdecl? Хотелось бы, что бы в коде происходило минимум лишних телодвижений, в том числе и самим компилятором, поскольку используются всего 3 регистра. Помогите правильно переписать на значения по ссылке, а то я боюсь что-нить где-нить не сохранить/восстановить или сделать лишнее. P.S. Кстати, не знаете, на Королевстве Дельфи при попытке отправить вопрос выдаёт ошибку, так уже больше года, там вообще есть кто живой? |
Сообщ.
#2
,
|
|
|
Откуда взялась такая процедура и какой от неё прок?
В дельфийской, естественно: procedure DivMod(Dividend: UInt64; Divisor: UInt64; var Result, Remainder: UInt64); overload; {$ELSEIF Defined(CPUX64)} asm MOV R10,RDX MOV RAX,RCX XOR EDX,EDX DIV R10 MOV [R8],RAX MOV [R9],RDX end; |
Сообщ.
#3
,
|
|
|
Цитата MBo @ Откуда взялась такая процедура и какой от неё прок? Вычисление подходящих дробей, не нашёл в языке процедуры, которая одновременно частное и остаток предоставляет, хотя инструкции процессора именно так и делают. Можно пояснить, по Асму, какие регистры можно использовать и почему не надо их сохранять\восстанавливать, а так же зачем тут оверлоад? |
Сообщ.
#4
,
|
|
|
Про регистры вот из хелпа
32-bit In general, the rules of register use in an asm statement are the same as those of an external procedure or function. An asm statement must preserve the EDI, ESI, ESP, EBP, and EBX registers, but can freely modify the EAX, ECX, and EDX registers. On entry to an asm statement, EBP points to the current stack frame and ESP points to the top of the stack. Except for ESP and EBP, an asm statement can assume nothing about register contents on entry to the statement. 64-bit In line with the x64 Application Binary Interface (ABI), the contents of the following registers must be preserved and restored within inline assembly functions: R12, R13, R14, R15, RDI, RSI, RBX, RBP, RSP, XMM4, XMM5, XMM6, XMM7, XMM8, XMM8, XMM9, XMM10, XMM11, XMM12, XMM13, XMM14, and XMM15. The first four parameters to inline assembler functions are passed via RCX, RDX, R8, and R9 respectively, except for floating-point arguments which use XMMO, XMM1, XMM2, XMM3. The math coprocessor is not normally used from x64 code. Registers used for function parameters can be modified freely. Оверлоад - поскольку есть еще процедура для Word со старых времён. P.S. На Королевстве при ошибке отправки ответа он часто все-таки отправляется, т.е. не стоит посылать заново - будет куча дубликатов, достаточно обновить страницу. Как с вопросами обстоит дело - не знаю. |
Сообщ.
#5
,
|
|
|
Цитата KIA @ не нашёл в языке процедуры, которая одновременно частное и остаток предоставляет, хотя инструкции процессора именно так и делают. DivMod есть в модуле Math. Просто добавь uses Math |
Сообщ.
#6
,
|
|
|
Я вот такое себе наваял
// Деление с остатком с одновременным получением частного и остатка. // Удобнее версии из Math, т.к. оперирует нативными числами. procedure DivMod(Dividend, Divisor: NativeUInt; var Result, Remainder: NativeUInt); {$IFDEF PUREPASCAL} begin Result := Dividend div Divisor; Remainder := Dividend mod Divisor; end; {$ELSE !PUREPASCAL} asm {$IFDEF CPU32} // => // EAX: Dividend // EDX: Divisor // ECX: [Result] // <= // EAX: [Result] // EDX: [Remainder] PUSH EBX MOV EBX, EDX // EBX := Divisor XOR EDX, EDX // EDX (старшее слово) := 0 DIV EBX // EAX := (EAX:EDX) div EBX, EDX := (EAX:EDX) div EBX MOV [ECX], EAX // Result := EAX MOV EBX, Remainder // Remainder := EDX MOV [EBX], EDX // POP EBX {$ENDIF} {$IFDEF CPU64} MOV R10,RDX MOV RAX,RCX XOR EDX,EDX DIV R10 MOV [R8],RAX MOV [R9],RDX {$ENDIF} {$IF NOT DEFINED(CPU32) AND NOT DEFINED(CPU64)} Unknown architecture {$IFEND} end; {$ENDIF !PUREPASCAL} Для 64-битных чисел под 32-битные CPU не стал мудрить, уж больно едрёный код получается |
Сообщ.
#7
,
|
|
|
Вроде бы лучше поменять местами "MOV [ECX], EAX" и "MOV EBX, Remainder", ибо последняя завязана на последующую "MOV [EBX], EDX" и может случиться небольшой простой...
|
Сообщ.
#8
,
|
|
|
Цитата Fr0sT @ Вроде бы лучше поменять местами "MOV [ECX], EAX" и "MOV EBX, Remainder", ибо последняя завязана на последующую "MOV [EBX], EDX" и может случиться небольшой простой... Можно и поменять, но современные процы с переупорядочиванием микроопераций сами все переставят, т.к. "MOV [ECX], EAX" "завязана" на результат предыдущей DIV, а "MOV EBX, Remainder" от нее не зависит, и соотв-но м.б. выполнена еще до окончания DIV (потенциально - как только кол-во оставшихся микроопераций DIV станет меньше общей длины очереди планировщика OoO). Добавлено Цитата Fr0sT @ // Удобнее версии из Math, т.к. оперирует нативными числами. procedure DivMod(Dividend, Divisor: NativeUInt; var Result, Remainder: NativeUInt); Для операции деления это "удобство" чревато дополнительными тормозами (по сравнению с кучей overload версий), т.к. DIV (в общем случае) выполняется тем быстрее, чем меньше размер операндов. Поэтому при использовании тормозных операций типа DIV лучше использовать не NativeUInt, а минимально достаточный размер операнда\переменной. |
Сообщ.
#9
,
|
|
|
Цитата leo @ Для операции деления это "удобство" чревато дополнительными тормозами (по сравнению с кучей overload версий), т.к. DIV (в общем случае) выполняется тем быстрее, чем меньше размер операндов. Поэтому при использовании тормозных операций типа DIV лучше использовать не NativeUInt, а минимально достаточный размер операнда\переменной. Ты имеешь в виду, сделать варианты под 1- и 2-байтовые целые? А есть ли разница? Мне кажется, она настолько незначительна, что на фоне затрат на вызов функции полностью потеряется. |
Сообщ.
#10
,
|
|
|
Цитата Fr0sT @ Ты имеешь в виду, сделать варианты под 1- и 2-байтовые целые? А есть ли разница? На современных компах разница в латентности DIV для 1-2-4 байтных целых действительно незначительная. А вот между r32 и r64 - весьма существенная. Поэтому с точки зрения быстродействия DivMod нужны overload варианты для UInt32=Cardinal и для UInt64. |
Сообщ.
#11
,
|
|
|
Цитата leo @ между r32 и r64 - весьма существенная Даже для х64? Потому что нативное целое вроде бы всегда самое быстрое. Добавлено Тем более что все равно параметры передаются и результат возвращается в регистрах нативного размера. Ну а если имеется в виду вариант с uint64 под x32, то я не стал заморачиваться с асмом (уж очень суровый код в Math ) и сделал по-тупому: {$IFDEF CPU32} // mod в 32-битной версии для 64-битных чисел очень медленный (в 20 раз медленней, // чем sub + div + mul!). Поэтому две функции-заменялки оператора mod и функции DivMod. function Mod64(A, B: Int64): Int64; begin Result := A - (A div B)*B; end; // Деление с остатком с одновременным получением частного и остатка. procedure DivMod(Dividend: UInt64; Divisor: UInt64; var Result, Remainder: UInt64); begin Result := Dividend div Divisor; Remainder := Dividend - Result*Divisor; end; {$ENDIF} |
Сообщ.
#12
,
|
|
|
Цитата Fr0sT @ Даже для х64? Потому что нативное целое вроде бы всегда самое быстрое. Удивительная наивность в понимании "нативность" (каламбурчик ) Во-первых, операция деления в принципе (чисто алгоритмически), выполняется тем дольше, чем больше размер операндов. Особенно наглядно это проявляется в сравнительно бесхитростных Pentium II, III и ниже (см. instruction_tables.pdf by Agner Fog research). Хотя реально время выполнения DIV зависит не от номинального размера операндов, а от реальных значений и соотношения значений делимого и делителя (которые становятся известны только во время исполнения, т.е. через 15 и более тактов = длине конвеера от стадии декодирования до исполнения). Это было учтено в последних моделях Pentium М и затем перекочевало в современную архитектуру Intel Core. Поэтому в современных x86(-64) минимальная задержка DIV для 1-2-4 байтных операндов, действительно различается мало (а максимальная зависит от реальных значений операндов). Однако для 64-битных операндов в x86-64 разница в задержке по сравнению с 32-битными (пока) остается существенной (см. упомянутую instruction_tables). PS: Теоретически и умножение, и даже сложение чисел большой разрядности должно выполняться дольше. Однако за счет повышения быстродействия (не номинальной частоты, а уменьшения переходных процессов в транзисторах\вентилях) и схемотехнических ухищрений удается уложить 64-битные операции в те же такты, что и 32-битные (плюс возможные неявные хитрости - например, основной результат умножения 64-бит в RAX вычисляется за то же кол-во тактов, что и 32-битный в EAX, но в случае переполнения RAX результат в RDX выдается на такт позже). Во-вторых, насколько я понимаю, речь идет не о 64-битной архитектуре вообще, а конкретно об x86-64 (= AMD-64 и Intel 64), которая по существу является "примочкой" (расширением) над IA-32 со всеми вытекающими отсюда особенностями и ограничениями. В частности, она создавалась в первую очередь для расширения потенциально адресуемой памяти до 64-бит (и дополнительно для увеличения кол-ва регистров до 16 штук), а не просто для увеличения разрядности РОН до 64 бит, которая большинству приложений нафиг не нужна (см. дискуссию о "перспективах" 128-битных РОН). В итоге в x86-64 размер операнда по умолчанию составляет 32 бита, а любые операции с 64-битными регистрами используют префикс REX, что как-то не вяжется с традиционными представлениями о "нативности" в предыдущих 16- и 32-битных архитектурах, в которых "нативными" были те операции, которые не требовали никаких префиксов. В итоге злоупотребление 64-битными операциями в x86-64 ведет к увеличению размера кода за счет REX-префиксов, а злоупотребление 64-битными данными в памяти (особенно в массивах) - к неэффективному использованию ОЗУ и кэша, со всеми вытекающими отсюда последствиями по быстродействию. Вот вам и вся пресловутая "нативность" Добавлено Цитата Fr0sT @ Тем более что все равно параметры передаются и результат возвращается в регистрах нативного размера. Не все равно, т.к. в x86-64 наконец-то отказались от доступа к старшей части 64-битных регистров, поэтому для беззнаковых данных с реальным размером до 32 бит регистры EAX и RAX по сути эквивалентны. Т.е. запись типа MOV EAX,EDX по сути эквивалента MOV RAX, RDX (с автоматическим обнулением старших разрядов RAX) но 1) короче по размеру кода, т.к. не требует лишнего байта префикса, 2) не вызывает никаких partial register stall при последующем обращении к RAX. Поэтому для DivMod без разницы, передается ли UInt32 через 32-битный или 64-битный регистр, но для (потенциального) сокращения времени выполнения лучше использовать не DIV r64, а DIV r32 с использованием результата хоть из EDX:EAX, хоть RDX:RAX - без разницы. |
Сообщ.
#13
,
|
|
|
leo, спасибо за просвещение! В очередной раз снимаю шляпу перед гуру
Вот что у меня в итоге получилось procedure DivMod(Dividend, Divisor: UInt32; var Result, Remainder: UInt32); asm {$IFDEF CPU64} MOV R10D,EDX MOV EAX,ECX XOR EDX,EDX DIV R10D MOV [R8],EAX MOV [R9],EDX {$ENDIF} ... end; Разница по времени выполнения - 2521 у нативного против 1253 у 32-битного варианта. Ну и финальный вариант // Деление с остатком с одновременным получением частного и остатка. // Удобнее версии из Math, т.к. оперирует числами одинаковой разрядности // ! Под CPU64 вариант с 32-битными операндами быстрее почти в 2 раза. Поэтому // не нативные типы, а явное указание размера procedure DivMod(Dividend, Divisor: UInt32; out Result, Remainder: UInt32); {$IFDEF PUREPASCAL} begin Result := Dividend div Divisor; Remainder := Dividend mod Divisor; end; {$ELSE !PUREPASCAL} asm {$IFDEF CPU32} // EAX: Dividend // EDX: Divisor // ECX: @Result PUSH EBX MOV EBX, Divisor XOR EDX, EDX // EDX (старшее слово для div) := 0 DIV EBX // EAX := (EDX:EAX) div EBX, EDX := (EDX:EAX) mod EBX MOV [Result], EAX MOV EBX, Remainder // Remainder := EDX MOV [EBX], EDX // POP EBX {$ENDIF} {$IFDEF CPU64} // RCX (ECX): Dividend // RDX (EDX): Divisor // R8: @Result // R9: @Remainder .NOFRAME MOV R10D, Divisor MOV EAX, Dividend XOR EDX, EDX // RDX (старшее двойное слово для div) := 0 DIV R10D // EAX := (EDX:EAX) div R10D, EDX := (EDX:EAX) mod R10D MOV [Result], EAX MOV [Remainder], EDX {$ENDIF} {$IF NOT DEFINED(CPU32) AND NOT DEFINED(CPU64)} Architecture not implemented {$IFEND} end; {$ENDIF !PUREPASCAL} procedure DivMod(Dividend, Divisor: UInt64; out Result, Remainder: UInt64); // mod в 32-битной версии для 64-битных чисел очень медленный (в 20 раз медленней, // чем sub + div + mul!). А asm код уж очень большой. {$IFDEF CPU32} begin Result := Dividend div Divisor; Remainder := Dividend - Result*Divisor; end; {$ENDIF} {$IFDEF CPU64} {$IFDEF PUREPASCAL} begin Result := Dividend div Divisor; Remainder := Dividend mod Divisor; end; {$ELSE !PUREPASCAL} asm // RCX: Dividend // RDX: Divisor // R8: @Result // R9: @Remainder .NOFRAME MOV R10, Divisor MOV RAX, Dividend XOR RDX, RDX // RDX (старшее двойное слово для div) := 0 DIV R10 // RAX := (RDX:RAX) div R10, RDX := (RDX:RAX) mod R10 MOV [Result], RAX MOV [Remainder], RDX end; {$ENDIF !PUREPASCAL} {$ENDIF CPU64} {$IF NOT DEFINED(CPU32) AND NOT DEFINED(CPU64)} Architecture not implemented {$IFEND} |
Сообщ.
#14
,
|
|
|
Цитата Fr0sT @ procedure DivMod(Dividend, Divisor: UInt64; out Result, Remainder: UInt64); // mod в 32-битной версии для 64-битных чисел очень медленный (в 20 раз медленней, // чем sub + div + mul!). А asm код уж очень большой. В общем случае - да. Но можно оптимизировать частные случаи с Int64Rec(Divisor).Hi = 0. procedure DivMod64_CPU32(Dividend, Divisor: UInt64; out Result, Remainder: UInt64); begin //если Divisor <= 32 бит if Int64Rec(Divisor).Hi = 0 then asm push edi push esi mov edi, eax //edi = @Result mov esi, edx //esi = @Remainder mov ecx, [dword ptr Divisor] //ecx = Divisor.Lo mov edx, [dword ptr Dividend + 4] //edx = Dividend.Hi xor eax,eax mov [esi + 4], eax //-> Remainder.Hi = 0 cmp edx,ecx //if Dividend.Hi >= Divisor.Lo then jb @@1 mov eax,edx xor edx,edx div ecx //eax = Result.Hi, edx - остаток Dividend.Hi < Divisor.Lo @@1: //else mov [edi+4], eax //-> Result.Hi = 0 или результат 1-го деления mov eax,[dword ptr Dividend] //eax = Dividend.Lo, edx = остаток Dividend.Hi < Divisor.Lo div ecx mov [edi], eax //-> Result.Lo mov [esi], edx //-> Remainder.Lo pop esi pop edi end else begin Result := Dividend div Divisor; Remainder := Dividend - Result*Divisor; end; end; При Divisor.Hi = 0 этот код дает выигрыш по скорости до 1.7-2 раз при Dividend.Hi = 0, и в 20 с лишним раз при Dividend.Hi > 0. (Цифры для Delphi 7 на Intel Core i5-3xxx SandyBridge). |
Сообщ.
#15
,
|
|
|
Толково! Жаль, Fastcode помер, а то можно было бы добавить в копилку.
|