我们如何使用循环展开和代码预加载为 C++ 程序编写优化的 ARM 代码?
How can we write optimized ARM code for C++ program using loop unrolling and code pre-loading?
下面是同一程序的给定 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
}
手臂代码:
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
我不知道 'code preload' 是什么意思。使用 pld
指令进行数据预加载。这在示例代码的上下文中是有意义的。
这是基本的 'C' 版本,给出了假设,
The size of the array n is large, and is a multiple of 64 elements and exclusive-OR bit-wise operation with the 8-bit mask and produces an output array 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;
}
}
您可以查看 output on 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
等,如果你保存它们并且不是异常安全的话。
来自评论,
I am trying to find that how many cycles does this subroutine take per
array element when it is optimized and when it isn't.
循环计数在现代 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
...
这(或一些排列)将允许 load/store 单元和 'eor' 不互相阻塞,但这将取决于特定的 CPU 类型。这个话题叫做指令调度;它比 pld
或数据预加载更强大。同样,您可以使用 NEON 或 ARM64 指令,以便循环体可以在 load/store.
之前执行更多 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
,祈祷会产生好的code。
编辑:您也可以尝试 this:
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;});
}
下面是同一程序的给定 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
}
手臂代码:
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
我不知道 'code preload' 是什么意思。使用 pld
指令进行数据预加载。这在示例代码的上下文中是有意义的。
这是基本的 'C' 版本,给出了假设,
The size of the array n is large, and is a multiple of 64 elements and exclusive-OR bit-wise operation with the 8-bit mask and produces an output array 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;
}
}
您可以查看 output on 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
等,如果你保存它们并且不是异常安全的话。
来自评论,
I am trying to find that how many cycles does this subroutine take per array element when it is optimized and when it isn't.
循环计数在现代 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
...
这(或一些排列)将允许 load/store 单元和 'eor' 不互相阻塞,但这将取决于特定的 CPU 类型。这个话题叫做指令调度;它比 pld
或数据预加载更强大。同样,您可以使用 NEON 或 ARM64 指令,以便循环体可以在 load/store.
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
,祈祷会产生好的code。
编辑:您也可以尝试 this:
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;});
}