当前位置: 首页 > 知识库问答 >
问题:

为什么ARM使用两条指令来屏蔽一个值?

桂高义
2023-03-14

对于以下功能...

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。编译器是否可以使用和r0,r0,#4294901760?


共有3个答案

于恺
2023-03-14

ARM是一个RISC机器(高级RISC机器),因此,所有指令都以相同的大小编码,上限为32位。

指令中的立即值被分配给一定数量的位,而AND指令根本没有足够的位分配给立即值来表示任何16位值。

这就是编译器转而使用两个移位指令的原因。

然而,如果您的目标CPU是ARMv6(ARM11)或更高版本,则编译器会利用新的REV16指令,然后通过UXTH指令屏蔽较低的16位,这是不必要且愚蠢的,但没有常规方法说服编译器不要这样做。

如果你认为GCC内在的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是不必要的。

在汇编中编写整个函数;这是唯一的选择。

养鸿运
2023-03-14

这里有一个遗漏的优化,但和不是遗漏的部分。生成16位常量并不便宜。对于一个循环来说,是的,在循环外生成一个常数并在循环内使用它是一种胜利。(待办事项:在数组上的循环中调用swap,看看我们得到了什么样的代码。)

对于无序的CPU,使用关键路径外的多条指令来构建常数也是值得的,那么关键路径上只有一个和两个移位,而不是两个移位。但这可能很罕见,而且不是gcc选择的。

AFAICT(从简单函数的编译器输出来看),ARM调用约定保证输入寄存器中没有高垃圾,并且不允许在返回值中留下高垃圾。i、 e.在输入时,可以假设r0的上16位都为零,但返回时必须保持为零。

(请注意,x86调用约定不是这样的:返回值允许有高垃圾(可能是因为调用方可以简单地使用16位或8位部分寄存器)。输入值也是如此,但作为x86-64系统V ABI的未记录部分除外:clang取决于输入值是否被符号/零扩展到32位。GCC在调用时提供此功能,但不假设为被调用方。)

ARMv6有一个rev16指令,用于字节交换寄存器的两个16位半部分。如果上面的16位已经归零,则不需要重新归零,因此gcc-mar=armv6应该将函数编译为仅rev16。但实际上它会发出一个uxth来提取和零扩展低半字。(即与0x0000FFFF完全相同,但不需要大常量)。我认为这纯粹是错过了优化;大概gcc的Rotate习惯用法,或者它以这种方式使用rev16的内部定义,没有包含足够的信息让它意识到上半部分保持零。

swap:                @@ gcc6.3 -O3 -march=armv6 -marm
    rev16   r0, r0
    uxth    r0, r0     @ not needed
    bx      lr

对于ARM v6之前的版本,可以使用较短的序列。GCC只有在我们将其朝着我们想要的asm手拿时才能找到它:

// 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,它也会编译成相同的代码。

常培
2023-03-14
匿名用户

旧的ARM程序集无法轻松创建常量。相反,它们被加载到文字池中,然后通过内存加载读入。您建议的这个只能采用带shift的8位文字。您的0xFFFF0000需要16位才能作为1条指令。

因此,我们可以从内存中加载并执行一个和(慢),执行2条指令来创建值和1到和(更长),或者只需廉价地移动两次就可以了。

编译器选择了移位,老实说,它非常快。

现在进行现实检查:

担心一个班次,除非这是一个百分之百确定的瓶颈,否则这是浪费时间。即使编译器是次优的,你也几乎不会感觉到。担心代码中的“热”循环,而不是像这样的微操作。从好奇心的角度来看这真是太棒了。担心这个代码在你的应用程序中的性能,并没有那么多。

编辑:

这里的其他人已经注意到,ARM规范的更新版本允许更高效地完成这类事情。这表明,在这个级别上讨论时,指定芯片或至少指定我们正在处理的ARM规范是很重要的。我是因为你的输出中没有“更新的”指令而假设古手臂的。如果我们正在跟踪编译器错误,那么这个假设可能不成立,了解规范更为重要。对于这样的交换,在以后的版本中确实有更简单的指令来处理。

编辑2

可以做的一件事是使其内联。在这种情况下,编译器可以将这些操作与其他工作交叉进行。根据CPU的不同,这可能会使吞吐量翻倍,因为许多ARM CPU有2个整数指令管道。将说明充分展开,这样就不会有危险,然后它就消失了。这必须与I-Cache的使用情况进行权衡,但在重要的情况下,您可以看到更好的结果。

 类似资料:
  • 使用指南 - 账户管理 - 屏蔽管理 为您详细介绍百度统计的账户结构,包括账户、站点、报告间的层级关系,报告或者消费数据的账户权限问题,以及假如您拥有多个账户,需要实现多账户授权管理的操作方法。 屏蔽管理 如何屏蔽IP和访客 屏蔽IP及访客标识码的效果

  • 问题内容: 当前,我们通常记录所有进出我们系统的XML文档,其中一些包含明文密码。我们希望能够配置执行此操作的logback logger / appender进行某种模式匹配或类似操作,并且如果它检测到存在替换它的密码(很可能带有星号)。注意,我们不想过滤掉日志条目,我们想掩盖其中的一部分。我很乐意提供有关如何通过注销执行此操作的建议。谢谢。 问题答案: 0.9.27版本的logback引入了替

  • 我有一个数组,我想屏蔽它,这样我就可以保持它的形状,即,不删除屏蔽的元素。 例如在此代码中 打印输入是对未屏蔽元素进行上述数学运算的结果,并返回一个没有屏蔽元素的1D数组。

  • 屏蔽IP 可以在此设置屏蔽蜘蛛爬的IP,点击右上角添加需要屏蔽的IP即可

  • 问题内容: 我创建了下面的角度指令, ChildDirective ,在 ParentDirective中使用 这正常工作,并且出现了几个子指令。 我想更新 ParentDirective ,以从服务器获取 childDirective 的列表。因此,我更新了 ParentDirective 代码以进行Ajax调用,然后绘制 ChildDirectives 问题是, childDirectives