如何使用循环展开和代码预加载为C++程序编写优化的ARM代码



下面是同一程序的给定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上查看输出。

ldmstm指令可用于将连续存储器地址加载到寄存器。我们不能使用所有的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中的阵列极限来检查R0R1。如果希望符合EABI,则需要保存所有被调用者保存的寄存器。寄存器组r4-r11通常可以使用,但取决于系统。如果您保存了lrfp等,并且它们不是异常安全的,您也可以使用它们。


根据评论,

我正试图找出这个子程序每数组元素。

在现代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;});
}

最新更新