为什么 ARM 使用两个指令来屏蔽一个值
Why does ARM use two instructions to mask a value?
对于以下函数...
uint16_t swap(const uint16_t value)
{
return value << 8 | value >> 8;
}
。为什么带有 -O2 的 ARM gcc 6.3.0 会产生以下程序集?
swap(unsigned short):
lsr r3, r0, #8
orr r0, r3, r0, lsl #8
lsl r0, r0, #16 # shift left
lsr r0, r0, #16 # shift right
bx lr
编译器似乎正在使用两个移位来屏蔽不需要的字节,而不是使用逻辑 AND。 编译器可以使用and r0, r0, #4294901760
吗?
较旧的 ARM 程序集无法轻松创建常量。相反,它们被加载到文本池中,然后通过内存加载读入。您建议的这个and
只能采用我相信带有移位的 8 位文字。您的0xFFFF0000
需要 16 位才能执行 1 条指令。
因此,我们可以从内存加载并执行and
(慢), 取 2 条指令来创建值,1 条指令和 (更长), 或者只是便宜地换两次并称其为好。
编译器选择了移位,老实说,它的速度非常快。
现在进行现实检查:
担心一个班次,除非这是100%的瓶颈,否则肯定是浪费时间。 即使编译器不是次优的,你也几乎永远不会感觉到它。 担心代码中的"热"循环,而不是像这样的微操作。 从好奇心来看这个真是太棒了。 担心这个确切的代码来提高你的应用的性能,而不是那么多。
编辑:
这里的其他人已经注意到,较新版本的ARM规范允许更有效地完成这种事情。 这表明,在这个级别上谈论时,指定芯片或至少我们正在处理的确切ARM规范非常重要。 我假设古代 ARM 是因为您的输出中缺乏"更新"指令。 如果我们跟踪编译器错误,那么这个假设可能站不住脚,了解规范更为重要。 对于这样的交换,在以后的版本中确实有更简单的说明来处理这个问题。
编辑 2
可以做的一件事是使其更快。 在这种情况下,编译器可以将这些操作与其他工作交错。 根据 CPU 的不同,这可能会使这里的吞吐量翻倍,因为许多 ARM CPU 都有 2 个整数指令管道。 将说明充分展开,以免有危险,然后就可以走了。 这必须与 I-Cache 使用情况进行权衡,但在重要的情况下,您可以看到更好的东西。
这里有一个遗漏的优化,但and
不是缺失的部分。 生成 16 位常量并不便宜。 对于循环,是的,在循环外生成常量并在循环内仅使用and
将是一个胜利。 (TODO:在数组上循环调用swap
,看看我们得到什么样的代码。
对于无序 CPU,还值得在关键路径之外使用多个指令来构建常量,然后关键路径上只有一个AND
而不是两个班次。 但这可能很少见,而不是 gcc 选择的。
AFAICT(通过查看简单函数的编译器输出),ARM 调用约定保证输入寄存器中没有高垃圾,并且不允许在返回值中留下高垃圾。 即在输入时,它可以假设r0
的高 16 位全部为零,但在返回时必须将它们保留为零。 因此,左移value << 8
是一个问题,但value >> 8
不是(它不必担心将垃圾向下移动到低 16)。
(请注意,x86 调用约定不是这样的:返回值允许具有高垃圾。 (也许是因为调用方可以简单地使用 16 位或 8 位部分寄存器)。 输入值也是如此,除了作为 x86-64 System V ABI 的未记录部分:clang 取决于将符号/零扩展到 32 位的输入值。 GCC 在呼叫时提供此功能,但不假定为被调用方。
ARMv6 有一个rev16
指令,该指令对寄存器的两个 16 位部分进行字节交换。 如果上面的 16 位已经归零,则不需要将它们重新归零,因此gcc -march=armv6
应该将函数编译为仅rev16
。 但实际上,它发出了一个uxth
来提取和零扩展低半字。 (即与and
0x0000FFFF
完全相同,但不需要大常数)。 我相信这纯粹是错过的优化;据推测,GCC 的旋转习语,或者它以这种方式使用rev16
的内部定义,没有包含足够的信息来让它意识到上半部分保持零。
swap: @@ gcc6.3 -O3 -march=armv6 -marm
rev16 r0, r0
uxth r0, r0 @ not needed
bx lr
对于 ARM pre v6,可以使用更短的序列。 只有当我们把它放在我们想要的 asm 上时,GCC 才能找到它:
// better on pre-v6, worse on ARMv6 (defeats rev16 optimization)
uint16_t swap_prev6(const uint16_t value)
{
uint32_t high = value;
high <<= 24; // knock off the high bits
high >>= 16; // and place the low8 where we want it
uint8_t low = value >> 8;
return high | low;
//return value << 8 | value >> 8;
}
swap_prev6: @ gcc6.3 -O3 -marm. (Or armv7 -mthumb for thumb2)
lsl r3, r0, #24
lsr r3, r3, #16
orr r0, r3, r0, lsr #8
bx lr
但这破坏了 gcc 的旋转习语识别,因此即使简单版本编译为rev16
/uxth
时,即使-march=armv6
,它也可以编译为相同的代码。
Godbolt 编译器资源管理器上的所有源代码 + asm
ARM是RISC机器(Advanced RISC Machine),因此,所有指令都以相同的大小编码,上限为32位。
指令中的即时值被分配给一定数量的位,而AND
指令根本没有足够的位分配给即时值来表示任何 16 位值。
这就是编译器诉诸两个移位指令的原因。
但是,如果您的目标 CPU 是 ARMv6 (ARM11) 或更高版本,编译器会利用新的REV16
指令,然后通过UXTH
指令屏蔽较低的 16 位,这是不必要和愚蠢的,但根本没有传统的方法来说服编译器不要这样做。
如果你认为海湾合作委员会内在__builtin_bswap16
会很好地为你服务,那你就大错特错了。
uint16_t swap(const uint16_t value)
{
return __builtin_bswap16(value);
}
上面的函数生成的机器代码与原始 C 代码完全相同。
即使使用内联程序集也无济于事
uint16_t swap(const uint16_t value)
{
uint16_t result;
__asm__ __volatile__ ("rev16 %[out], %[in]" : [out] "=r" (result) : [in] "r" (value));
return result;
}
同样,完全相同。只要您使用 GCC,就无法摆脱讨厌的UXTH
;它根本无法从上下文中读取高 16 位都是零开始,因此UXTH
是不必要的。
在汇编中编写整个函数;这是唯一的选择。
这是最佳解决方案,AND 至少需要另外两条指令,可能必须停止并等待要屏蔽的值发生加载。 所以在几个方面更糟。
00000000 <swap>:
0: e1a03420 lsr r3, r0, #8
4: e1830400 orr r0, r3, r0, lsl #8
8: e1a00800 lsl r0, r0, #16
c: e1a00820 lsr r0, r0, #16
10: e12fff1e bx lr
00000000 <swap>:
0: ba40 rev16 r0, r0
2: b280 uxth r0, r0
4: 4770 bx lr
后者是armv7,但同时是因为他们添加了支持此类工作的指令。
根据定义,固定长度的RISC指令存在常量问题。 MIPS选择了一条路,ARM选择了另一种方式。 常量在CISC上也是一个问题,只是一个不同的问题。 创建利用 ARMS 桶形移位器并显示 MIPS 解决方案缺点的东西并不困难,反之亦然。
该解决方案实际上具有一点优雅。
其中一部分也是目标的整体设计。
unsigned short fun ( unsigned short x )
{
return(x+1);
}
0000000000000010 <fun>:
10: 8d 47 01 lea 0x1(%rdi),%eax
13: c3 retq
gcc 选择不返回您要求的 16 位变量,它返回 32 位,它没有正确/正确实现我用代码要求的功能。 但是,如果当数据的用户获得该结果或使用它时,掩码发生在那里或使用此架构使用 ax 而不是 eax,那没关系。例如。
unsigned short fun ( unsigned short x )
{
return(x+1);
}
unsigned int fun2 ( unsigned short x )
{
return(fun(x));
}
0000000000000010 <fun>:
10: 8d 47 01 lea 0x1(%rdi),%eax
13: c3 retq
0000000000000020 <fun2>:
20: 8d 47 01 lea 0x1(%rdi),%eax
23: 0f b7 c0 movzwl %ax,%eax
26: c3 retq
编译器设计选择(可能基于体系结构)而不是实现错误。
请注意,对于足够规模的项目,很容易发现错过的优化机会。 没有理由期望优化器是完美的(它不是,也不可能是)。 对于平均而言,他们只需要比人工完成该规模项目的效率更高。
这就是为什么人们常说,对于性能调优,您不会预先优化或立即跳转到asm,您使用高级语言和编译器,以某种方式分析以找到性能问题,然后手动编码,为什么要手动编码它们,因为我们知道我们有时可以执行编译器, 这意味着编译器输出可以改进。
这不是一个错过的优化机会,相反,这是一个非常优雅的指令集解决方案。 屏蔽字节更简单
unsigned char fun ( unsigned char x )
{
return((x<<4)|(x>>4));
}
00000000 <fun>:
0: e1a03220 lsr r3, r0, #4
4: e1830200 orr r0, r3, r0, lsl #4
8: e20000ff and r0, r0, #255 ; 0xff
c: e12fff1e bx lr
00000000 <fun>:
0: e1a03220 lsr r3, r0, #4
4: e1830200 orr r0, r3, r0, lsl #4
8: e6ef0070 uxtb r0, r0
c: e12fff1e bx lr
后者是 ARMv7,但使用 ARMv7 他们识别并解决了这些问题,你不能指望程序员总是使用自然大小的变量,有些人觉得需要使用不太优化大小的变量。 有时您仍然需要遮罩到一定大小。
- 如何创建一个CMake变量,除非显式重写,否则使用默认值
- 删除一个线程上有数百万个字符串的大型哈希映射会影响另一个线程的性能
- 为什么两个不同的未命名名称空间可以共存于一个cpp文件中
- 运行同一解决方案的另一个项目的项目
- 挂起和取消挂起一个文件DLL
- 用C++中的一个变量定义一个常量
- 函数向量_指针有不同的原型,我可以构建一个吗
- 在c++中用vector填充一个简单的动态数组
- 如何在选项卡视图Qt中设置一个新项目,并保存以前的项目
- 预处理器:插入结构名称中的前一个行号
- 我在c++代码中生成了一个运行时#3异常
- 我想将一个对T类型的非常量左值引用绑定到一个T类型的临时值
- 从链接列表c++中删除一个项目
- 告诉一个 const char 数组,除了编译时 C 样式的字符串外,它不以 '