我正在尝试运行以下命令,
type
Vector = array [1..4] of Single;
{$CODEALIGN 16}
function add4(const a, b: Vector): Vector; register; assembler;
asm
movaps xmm0, [a]
movaps xmm1, [b]
addps xmm0, xmm1
movaps [@result], xmm0
end;
它在 movaps 上给出了访问冲突,据我所知,如果内存位置是 16 对齐,则 movaps 是可以信任的。如果movups(不需要对齐),它就没有问题。
所以我的问题是,在 Delphi XE3 中,{$CODEALIGN} 在这种情况下似乎不起作用。
编辑
很奇怪...我尝试了以下方法。
program Project3;
{$APPTYPE CONSOLE}
uses
windows; // if not using windows, no errors at all
type
Vector = array [1..4] of Single;
function add4(const a, b: Vector): Vector;
asm
movaps xmm0, [a]
movaps xmm1, [b]
addps xmm0, xmm1
movaps [@result], xmm0
end;
procedure test();
var
v1, v2: vector;
begin
v1[1] := 1;
v2[1] := 1;
v1 := add4(v1,v2); // this works
end;
var
a, b, c: Vector;
begin
{$ifndef cpux64}
{$MESSAGE FATAL 'this example is for x64 target only'}
{$else}
test();
c := add4(a, b); // throw out AV here
{$endif}
end.
如果不添加“使用窗口”,则一切正常。 如果“使用窗口”,那么它将在 c := add4(a, b) 处抛出异常,但不会在 test() 处抛出异常。
谁能解释一下?
编辑 现在这对我来说一切都有意义了。 Delphi XE3 - 64 位的结论是
您需要数据按 16 字节对齐。这需要一些照顾和关注。您可以确保堆分配器与 16 字节对齐。但是您无法确保编译器会对堆栈分配的变量进行 16 字节对齐,因为您的数组的对齐属性为 4,即其元素的大小。在其他结构内声明的任何变量也将具有 4 字节对齐。这是一个很难克服的障碍。
我认为您无法在当前可用的编译器版本中解决您的问题。至少不会,除非你放弃堆栈分配的变量,我猜这是一颗难以下咽的药丸。您可能会幸运地使用外部汇编器。
您可以编写自己的内存分配例程,在堆中分配对齐的数据。您可以指定自己的对齐大小(不仅是 16 字节,还可以是 32 字节、64 字节等等......):
procedure GetMemAligned(const bits: Integer; const src: Pointer;
const SrcSize: Integer; out DstAligned, DstUnaligned: Pointer;
out DstSize: Integer);
var
Bytes: NativeInt;
i: NativeInt;
begin
if src <> nil then
begin
i := NativeInt(src);
i := i shr bits;
i := i shl bits;
if i = NativeInt(src) then
begin
// the source is already aligned, nothing to do
DstAligned := src;
DstUnaligned := src;
DstSize := SrcSize;
Exit;
end;
end;
Bytes := 1 shl bits;
DstSize := SrcSize + Bytes;
GetMem(DstUnaligned, DstSize);
FillChar(DstUnaligned^, DstSize, 0);
i := NativeInt(DstUnaligned) + Bytes;
i := i shr bits;
i := i shl bits;
DstAligned := Pointer(i);
if src <> nil then
Move(src^, DstAligned^, SrcSize);
end;
procedure FreeMemAligned(const src: Pointer; var DstUnaligned: Pointer;
var DstSize: Integer);
begin
if src <> DstUnaligned then
begin
if DstUnaligned <> nil then
FreeMem(DstUnaligned, DstSize);
end;
DstUnaligned := nil;
DstSize := 0;
end;
然后使用指针和过程作为第三个参数来返回结果。
你也可以使用函数,但不是那么明显。
type
PVector^ = TVector;
TVector = packed array [1..4] of Single;
然后以这种方式分配这些对象:
const
SizeAligned = SizeOf(TVector);
var
DataUnaligned, DataAligned: Pointer;
SizeUnaligned: Integer;
V1: PVector;
begin
GetMemAligned(4 {align by 4 bits, i.e. by 16 bytes}, nil, SizeAligned, DataAligned, DataUnaligned, SizeUnaligned);
V1 := DataAligned;
// now you can work with your vector via V1^ - it is aligned by 16 bytes and stays in the heap
FreeMemAligned(nil, DataUnaligned, SizeUnaligned);
end;
正如您所指出的,我们已将
nil
传递给 GetMemAligned 和 FreeMemAligned - 当我们想要对齐现有数据时需要此参数,例如例如,我们作为函数参数收到的一个。
在汇编例程中只使用直接的寄存器名称而不是参数名称。使用寄存器调用约定时,您不会弄乱任何东西 - 否则您将面临修改寄存器的风险,而不知道所使用的参数名称只是寄存器的别名。
在Win64下,按照Microsoft调用约定,第一个参数始终作为RCX传递,第二个参数是RDX,第三个参数是R8,第四个参数是R9,其余参数在堆栈中传递。函数返回结果 拉克斯。但是,如果函数返回结构(“记录”)结果,则它不会在 RAX 中返回,而是按地址在隐式参数中返回。 以下寄存器可能会在调用后由您的函数修改:RAX、RCX、RDX、R8、R9、R10、R11。其余的应保留。 请参阅https://msdn.microsoft.com/en-us/library/ms235286.aspx了解更多详细信息。
在Win32下,使用Delphi寄存器调用约定,调用在EAX中传递第一个参数,在EDX中传递第二个参数,在ECX中传递第三个参数,然后在堆栈中传递
下表总结了差异:
64 32
--- ---
1) rcx eax
2) rdx edx
3) r8 ecx
4) r9 stack
因此,您的函数将如下所示(32 位):
procedure add4(const a, b: TVector; out Result: TVector); register; assembler;
asm
movaps xmm0, [eax]
movaps xmm1, [edx]
addps xmm0, xmm1
movaps [ecx], xmm0
end;
64位以下;
procedure add4(const a, b: TVector; out Result: TVector); register; assembler;
asm
movaps xmm0, [rcx]
movaps xmm1, [rdx]
addps xmm0, xmm1
movaps [r8], xmm0
end;
顺便说一句,根据 Microsoft 的说法,64 位调用约定中的浮点参数直接在 XMM 寄存器中传递:第一个在 XMM0 中,第二个在 XMM1 中,第三个在 XMM2 中,第四个在 XMM3 中,其余在堆栈中。所以你可以通过值传递它们,而不是通过引用。
使用它使内置内存管理器以 16 字节对齐方式分配:
SetMinimumBlockAlignment(mba16Byte);
此外,据我所知,“寄存器”和“汇编器”都是冗余指令,因此您可以从代码中跳过它们。
--
编辑:你提到这是针对 x64 的。我刚刚在为 x64 编译的 Delphi XE2 中尝试了以下内容,它在这里工作。
program Project3;
type
Vector = array [1..4] of Single;
function add4(const a, b: Vector): Vector;
asm
movaps xmm0, [a]
movaps xmm1, [b]
addps xmm0, xmm1
movaps [@result], xmm0
end;
procedure f();
var
v1,v2 : vector;
begin
v1[1] := 1;
v2[1] := 1;
v1 := add4(v1,v2);
end;
begin
{$ifndef cpux64}
{$MESSAGE FATAL 'this example is for x64 target only'}
{$else}
f();
{$endif}
end.
为了确保未打包记录类型中字段的正确对齐,编译器会在对齐为 2 的字段之前插入一个未使用的字节,并在对齐为 4 的字段之前插入最多 3 个未使用的字节(如果需要)。最后,编译器将记录的总大小向上舍入到由任何字段的最大对齐指定的字节边界。
https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Internal_Data_Formats_(Delphi)#Record_Types
对于下一条记录,如果使用 {$ALIGN 16},我们有下一个表
type
TAlignType = Extended;
TRec = record
first : byte;
second: TAlignType;
end;
TAlignType | Byte Word Integer Int64 Extended
-------------------------------------------------------------------
Record Align | 1 2 4 8 16
Record Size | 2 4 8 16 32
|
Align bytes | 0 1 3 7 15
after field first|
因此,为了使结构体对齐到16字节,必须添加大于8字节的字段
此示例在 x86/x64 中运行良好。 我添加了扩展类型的虚拟字段。
{$EXTENDEDCOMPATIBILITY ON} // To use 10 byte size for Win64. Extended type has 8 byte size for Win64 and 10 byte for Win32.
{$ALIGN 16}
type
Vector = record
case Integer of
0: (dw: array[0..4-1]of Single);
8: (a: Extended); // 10 byte field to use alignment of record field
// https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Internal_Data_Formats_(Delphi)#Record_Types
end;
function add4(const a, b: Vector): Vector;
asm
movaps xmm0, [a]
movaps xmm1, [b]
addps xmm0, xmm1
movaps [@result], xmm0
end;
procedure test();
var
dump: Integer; // 4 byte
a, b: Vector;
begin
a.dw[0] := 1;
b.dw[0] := 1;
a := add4(a, b); // this works
end;
var
a, b, c: Vector;
begin
test();
c := add4(a, b); // throw out AV here
Readln;
end.