下面是同一程序的给定c++和ARM代码。你能告诉我这个ARM代码是否优化了吗?循环需要多少(数组n的大小很大,是64个元素的倍数,与8位掩码进行异或逐位运算,并产生输出数组outArr。(?我应该如何使用循环展开来优化代码(一次处理4个元素(?
c++代码:
// Gray scale image pixel inversion
void invert(unsigned char *outArr, unsigned char *inArr,
unsigned char k, int n)
{
for(int i=0; i<n; i++)
*outArr++ = *inArr++ ^ k; // ^ is bitwise xor
}
ARM代码:
invert:
cmp r3, #0
bxle lr
add ip, r0, r3
.L3:
ldrb r3, [r1], #1 @ zero_extendqisi2
eor r3, r3, r2
strb r3, [r0], #1
cmp ip, r0
bne .L3
bx lr
我不知道"代码预加载"是什么意思。pld
指令预加载了数据。这在示例代码的上下文中是有意义的。
这是给定假设的基本"C"版本,
数组n的大小很大,是64个元素的倍数,与8位掩码进行异或逐位运算,并产生输出数组outArr。
该代码可能并不完美,但意在说明问题。
// Gray scale image pixel inversion
void invert(unsigned char *outArr, unsigned char *inArr,
unsigned char k, int n)
{
unsigned int *out = (void*)outArr;
unsigned int *in = (void*)inArr;
unsigned int mask = k<<24|k<<16|k<<8|k;
/* Check arguments */
if( n % 64 != 0) return;
if((int)outArr & 3) return;
if((int)inArr & 3) return;
assert(sizeof(int)==4);
for(int i=0; i<n/sizeof(int); i+=64/(sizeof(int)) {
/* 16 transfers per loop 64/4 */
*out++ = *in++ ^ k; // 1
*out++ = *in++ ^ k;
*out++ = *in++ ^ k;
*out++ = *in++ ^ k;
*out++ = *in++ ^ k; // 5
*out++ = *in++ ^ k;
*out++ = *in++ ^ k;
*out++ = *in++ ^ k;
*out++ = *in++ ^ k; // 9
*out++ = *in++ ^ k;
*out++ = *in++ ^ k;
*out++ = *in++ ^ k;
*out++ = *in++ ^ k; // 13
*out++ = *in++ ^ k;
*out++ = *in++ ^ k;
*out++ = *in++ ^ k;
}
}
您可以在godbolt上查看输出。
ldm
和stm
指令可用于将连续存储器地址加载到寄存器。我们不能使用所有的16个ARM寄存器,所以汇编程序中循环的核心看起来像这样,
ldmia [r1], {r4,r5,r6,r7,r8,r9,r10,r11} # r1 is inArr
eor r4,r4,r2 # r2 is expanded k
eor r5,r5,r2
eor r6,r6,r2
eor r7,r7,r2
eor r8,r8,r2
eor r9,r9,r2
eor r10,r10,r2
eor r11,r11,r2
stmia [r0], {r4,r5,r6,r7,r8,r9,r10,r11} # r0 is outArr
这重复两次,并且可以对照存储在R3
中的阵列极限来检查R0
或R1
。如果希望符合EABI,则需要保存所有被调用者保存的寄存器。寄存器组r4
-r11
通常可以使用,但取决于系统。如果您保存了lr
、fp
等,并且它们不是异常安全的,您也可以使用它们。
根据评论,
我正试图找出这个子程序每数组元素。
在现代CPU上,循环计数极其困难。然而,核心有五条指令,其中有一个简单的循环,
.L3:
ldrb r3, [r1], #1 @ zero_extendqisi2
eor r3, r3, r2
strb r3, [r0], #1
cmp ip, r0
bne .L3
要执行32字节,这是32*5(160(条指令。具有32*2内存访问。
扩展的选项只是一个32字节的内存读写。这些将完成,首先使用可用的最低值。然后只有一条EOR
指令。所以它只是10条指令,而不是160条。在现代处理器上,内存将是限制因素。由于内存停滞,一次只处理四个单词可能会更好,比如
ldmia [r1], {r4,r5,r6,r7} # r1 is inArr
eor r4,r4,r2 # r2 is expanded k
eor r5,r5,r2
eor r6,r6,r2
eor r7,r7,r2
ldmia [r1], {r8,r9,r10,r11} # r1 is inArr
stmia [r0], {r4,r5,r6,r7} # r0 is outArr
...
这(或一些排列(将允许加载/存储单元和"eor"不相互阻塞,但这将取决于特定的CPU类型。这个主题叫做指令调度;它比CCD_ 12或数据预加载更强大。此外,您还可以使用NEON或ARM64指令,以便循环主体可以在加载/存储之前执行更多的eor
操作。
现在,这是这样做的:
void invert(unsigned char* const outArr, unsigned char const* const inArr,
unsigned char const k, std::size_t const n) noexcept
{
std::transform(std::execution::unseq, inArr, inArr + n, outArr,
[k](auto const i)noexcept{return i ^ k;});
}
您设置了-Ofast
,交叉手指,希望生成好的代码。
编辑:你也可以试试这个:
void invert(unsigned char* const outArr, unsigned char const* const inArr,
unsigned char const k, std::size_t const n) noexcept
{
std::transform(std::execution::unseq,
reinterpret_cast<std::uint32_t const*>(inArr),
reinterpret_cast<std::uint32_t const*>(inArr) + n/4,
reinterpret_cast<std::uint32_t*>(outArr),
[k=std::uint32_t(k<<24|k<<16|k<<8|k)](auto const i)noexcept{return i ^ k;});
}