从字节数组加载 uint8x16_t 的对齐要求?
Posted
技术标签:
【中文标题】从字节数组加载 uint8x16_t 的对齐要求?【英文标题】:Alignment requirements for uint8x16_t being loaded from byte array? 【发布时间】:2016-05-28 18:34:30 【问题描述】:我们在 Debug 构建下触发了一个断言来检查对齐。断言适用于使用vld1q_u8
加载到uint8x16_t
中的字节数组。当断言触发时,我们没有观察到SIG_BUS
。
代码中的用法如下:
const byte* input = ...;
...
assert(IsAlignedOn(input, GetAlignmentOf(uint8x16_t));
uint64x2_t message = vreinterpretq_u64_u8(vld1q_u8(input));
我也尝试了以下方法,断言触发uint8_t*
的对齐:
assert(IsAlignedOn(input, GetAlignmentOf(uint8_t*));
uint64x2_t message = vreinterpretq_u64_u8(vld1q_u8(input));
将字节数组加载到带有vld1q_u8
的uint8x16_t
时,其对齐要求是什么?
在上面的代码中,input
是一个函数参数。 IsAlignedOn
检查其两个参数的对齐方式,确保第一个参数至少与第二个参数对齐。 GetAlignmentOf
是一种抽象,用于检索类型或变量的对齐方式。
uint8x16_t
和 uint64x2_t
是 128 位 ARM NEON 矢量数据类型,它们是 expected to be placed in a Q register。 vld1q_u8
是一个 NEON 伪指令,预计将被编译成 VLD1.8
指令。 vreinterpretq_u64_u8
是一个 NEON 伪指令,可简化数据类型的使用。
【问题讨论】:
代码不是C。 @Olaf - 我不确定你是否正确。它们是内在函数,which are a C language extension。引用的 GCC 文档指的是 ARM 文档,因此如果您想阅读它们,您应该同时拥有这两个参考。 @Olaf - 我们的源代码用于构建多个平台和多个编译器。编译器包括 GCC、Clang、MSVC。这些平台包括 Linux、Windows Phone 和 Windows Store。GetAlignmentOf
只是一个抽象。 (很多人没有意识到 Microsoft 编译器使用 ARM 内部函数)。
这几乎完全是特定于编译器的,因为它完全取决于它们如何实现内部类型以及是否要将对齐提示添加到底层指令中。据我所见,即使保证对齐,GCC也不会发出提示; Clang 倾向于尽可能地这样做。不知道MSVC。至于它们中的任何一个是否正确实现了向量类型,或者只是将它们类型定义为struct long long[2]
(导致对齐过于严格),我从来没有看过。
我已经给你提示了。没必要粗鲁。 _Alignas
等是标准 C。只需使用现代编译器,它不会被 27 年前的标准版本卡住。
【参考方案1】:
VLD1.8
指令的自然对齐,将 16 个字节加载到 Quad 寄存器,是一个字节。这意味着即使不允许未对齐的传输,该指令也不会出错。
所以看起来这个特定的断言是不正确的。
【讨论】:
虽然VLD1.8 ..., [Rn:64]
即使在正常的非对齐访问模型下也肯定会出错。
@Dric512 - 是byte
还是byte*
?我认为区别是 1 和 4。在这一点上,我只知道它不是 uint8x16_t
,因为我没有看到 SIG_BUS
因为缺少 16 字节对齐。我签入了patch to back-off the assert to an uint8_t*
,所以我应该很快就会从our test script 得到一些结果。
通常使用这样的优化来加速代码。未对齐的访问实际上在许多平台上执行操作。例如。它们可能会被分解为 1/2/4/... 字节范围的访问。
@Dric512 - 好的,所以它也不是byte*
或uint8_t*
。断言仍在触发;但它也不是SIG_BUS
'ing。此外,这也出现在 ARMv8 和 GCC 4.9 上,因此对齐可能是 8。
@jww。而不是实际的assert
,也许可以测试一些可以让你在那时打印出更多调试信息的东西。例如打印您要加载的实际地址。或者只是触发带有调试器的断言。这样你就不必猜测对齐是什么,而且你可以看到哪个函数传入了未对齐的指针。【参考方案2】:
在编写直接汇编程序(内联或在外部文件中)时,您可以选择是否要指定对齐方式(例如vld1.8 q0, [r0, :64]
)或不指定对齐方式(例如vld1.8 q0, [r0]
)。如 Dric512 所说,如果未指定,则根本不需要任何特定对齐。
当通过内在函数使用vld1q_u8
时,您实际上并没有指定对齐方式,据我所知,编译器不会假设它,并且会生成没有对齐规范的指令。我不确定某些编译器是否可以推断出某些实际保证对齐的情况并在这些情况下使用对齐说明符。 (在这种特殊情况下,gcc、clang 和 MSVC 似乎都生成了没有对齐说明符的 vld1.8
。)
请注意,这只是 32 位 arm 的问题;在 AArch64 中,ld1
指令没有对齐说明符。但即便如此,对齐仍然明显有帮助,如果将它与未对齐的地址一起使用,性能会更差。
【讨论】:
【参考方案3】:从另一端来看,这是从一个示例编译器(Visual Studio 2015 的arm_neon.h
)的角度来看该类型的实际定义:
typedef union __declspec(intrin_type) _ADVSIMD_ALIGN(8) __n128
unsigned __int64 n128_u64[2];
unsigned __int32 n128_u32[4];
unsigned __int16 n128_u16[8];
unsigned __int8 n128_u8[16];
__int64 n128_i64[2];
__int32 n128_i32[4];
__int16 n128_i16[8];
__int8 n128_i8[16];
float n128_f32[4];
struct
__n64 low64;
__n64 high64;
DUMMYNEONSTRUCT;
__n128;
...
typedef __n128 int8x16_t;
因此,至少在 Windows 平台上,由于该联合,它需要不少于 __int64
的对齐方式,并且从 AAPCS 开始,这意味着 8 个字节(即使没有不太具有挑战性的猜猜_ADVSIMD_ALIGN(8)
可能意味着什么……)
不过,它甚至比这更简单,因为事实证明,AAPCS 实际上确实直接在这方面拥有最后一个词,通过它根据 容器化向量 定义向量类型(第 4.1.1 节)。 2):
容器化向量的内容对于大多数过程调用标准是不透明的:其布局的唯一定义方面是内存格式(基本类型存储在内存中的方式)和不同类别的寄存器之间的映射过程调用接口。
换句话说,在 ABI 级别,向量类型是向量类型,无论其中可能包含或不包含什么,并且 64 位和 128 位容器化向量都需要 8 字节对齐,因为 ABI 说所以(§4.1)。因此,无论底层指令能够做什么,Microsoft 的实现都没有像我最初猜测的那样过于严格,它只是符合要求。 八是你要对齐的数字,对齐的数字是八。
另一方面,vld1q_u8()
的参数是 uint8_t const *
,其指向的数据没有对齐要求,因此断言它满足 8 字节对齐可能会失败很多。
【讨论】:
这不是有点正交吗?这是关于 int8x16_t 存储在某处时的对齐方式,但在大多数情况下,您希望它只保留在 NEON 寄存器中。这不会影响您可以使用vld1q_u8()
从指向未对齐地址的任何指针将数据加载到其中的情况,我相信这是 OP 所要求的。
@mstorsjo 其他答案已经很好地解决了 direct 问题。我认为这似乎也值得澄清为什么被问及的代码是错误的,尽管似乎我确实完全隐含了结论 - 已修复!
Notlikethat 和 mstorsjo - 我认为这些是来自 ARM 的控制文档:VLDn and VSTn (single n-element structure to one lane)、VLDn (single n-element structure to all lanes) 和 VLDn and VSTn (multiple n-element structures)。当我想要做很多事情时,我无法重新找到它们(而且我不能要求提供关于 SO 的参考)。
ABI 指的是 int8x8_t 等向量类型在作为此类类型访问时的对齐方式。如果将它们作为参数传递给函数,或者将它们包含在结构定义中,就会发生这种情况。在 vld1_s8 的情况下,参数是一个普通的 C int8_t*,它的对齐有普通的 C 规则以上是关于从字节数组加载 uint8x16_t 的对齐要求?的主要内容,如果未能解决你的问题,请参考以下文章