Sta*_*tan 6 c++ gcc compiler-optimization
我在 Linux 上使用 gcc 12.2。我使用时-nostdlib,编译器抱怨缺少 memcpy 和 memmove。所以我在汇编中实现了一个错误的memcpy,并且我让memmove调用中止,因为我总是想使用memcpy。
我想知道如果我用 C 实现自己的函数,是否可以避免编译器要求 memcpy (和 memmove)。优化器似乎注意到它到底是什么,并无论如何调用 C 函数。然而,自从它被实现(我使用#define memcpy mymemcpy)并且自从我运行它以来,我看到我的应用程序中止。它调用了我的 memmove 实现而不是程序集 memcpy。为什么 gcc 调用 move 而不是 copy?
clang 调用 memcpy 但 gcc 更好地优化了我的代码,因此我使用它来优化构建
__attribute__ ((access(write_only, 1))) __attribute__((nonnull(1, 2)))
inline void mymemcpy(void *__restrict__ dest, const void *__restrict__ src, int size)
{
const unsigned char *s = (const unsigned char*)src;
unsigned char *d = (unsigned char*)dest;
while(size--) *d++ = *s++;
}
Run Code Online (Sandbox Code Playgroud)
可重现
//dummy.cpp
extern "C" {
void*malloc() { return 0; }
int read() { return 0; }
int write() { return 0; }
int memcpy() { return 0; }
int memmove() { return 0; }
}
//main.cpp
#include <unistd.h>
#include <cstdlib>
struct MyVector {
void*p;
long long position, length;
};
__attribute__ ((access(write_only, 1))) __attribute__((nonnull(1, 2)))
void mymemcpy(void *__restrict__ dest, const void *__restrict__ src, int size)
{
const unsigned char *s = (const unsigned char*)src;
unsigned char *d = (unsigned char*)dest;
while(size--) *d++ = *s++;
}
//__attribute__ ((noinline))
int func(const char*file_from_disk, MyVector*v)
{
if (v->position + 5 <= v->length ) {
mymemcpy(v->p, file_from_disk, 5);
}
return 0;
}
char buf[4096];
extern "C"
int _start() {
MyVector v{malloc(1024),0,1024};
v.position += read(0, v.p, 1024-5);
int len = read(0, buf, 4096);
func(buf, &v);
write(1, v.p, v.position);
}
Run Code Online (Sandbox Code Playgroud)
g++ -march=native -nostdlib -static -fno-exceptions -fno-rtti -O2 main.cpp dummy.cpp
检查使用objdump -D a.out | grep call
401040: e8 db 00 00 00 call 401120 <memmove>
40108d: e8 4e 00 00 00 call 4010e0 <malloc>
4010a3: e8 48 00 00 00 call 4010f0 <read>
4010ba: e8 31 00 00 00 call 4010f0 <read>
4010c5: e8 56 ff ff ff call 401020 <_Z4funcPKcP8MyVector>
4010d5: e8 26 00 00 00 call 401100 <write>
402023: ff 11 call *(%rcx)
Run Code Online (Sandbox Code Playgroud)
准确的答案需要深入研究 GCC 执行的代码转换,并查看 GCC 如何转换您的代码。这超出了我在合理时间内所能做的事情,但我可以向您展示更一般性的情况,而无需深入了解 GCC 的内部结构。
这是疯狂的部分:如果删除inline,您将得到memcpy。有了inline,你就得到了memmove。我将在 Godbolt 上展示结果,然后讨论编译器如何工作来解释它。
这是我在Godbolt上放置的一些测试代码。
__attribute__ ((access(write_only, 1))) __attribute__((nonnull(1, 2)))
extern inline void mymemcpy(void *__restrict__ dest, const void *__restrict__ src, int size)
{
const unsigned char *s = (const unsigned char*)src;
unsigned char *d = (unsigned char*)dest;
while(size--) *d++ = *s++;
}
void test(void *dest, const void *src, int size)
{
mymemcpy(dest, src, size);
}
Run Code Online (Sandbox Code Playgroud)
这是最终的组装结果
mymemcpy:
test edx, edx
je .L1
mov edx, edx
jmp memcpy
.L1:
ret
test:
test edx, edx
je .L4
mov edx, edx
jmp memmove
.L4:
ret
Run Code Online (Sandbox Code Playgroud)
是的,您可以看到一个函数正在转换为memcpyor memmove。它不仅仅是相同的代码,它只是一个函数,根据它是否内联,它会进行不同的转换。为什么?
您可能会认为 C 编译器会执行以下操作:
预处理+标记化源文件,
解析创建 AST,
类型检查,
优化,
发出代码。
实际上,“优化”项是对代码的许多不同的传递,并且每个传递都以不同的方式修改代码。这些遍在编译期间的不同时间发生,并且某些优化遍可能发生多次。
特定优化过程发生的顺序会影响结果。如果先执行优化 X,然后执行优化 Y,则与先执行 Y,然后再执行 X 会得到不同的结果。也许一个转换将信息从程序的一个部分传播到另一部分,然后另一个转换对该信息起作用。
为什么这与此相关?
您可以在这里看到有一个restrict指针src和dest。由于这些指针是restrict,GCC“应该”能够知道这memcpy是可以接受的,并且memmove不是必需的。
src然而,这意味着和dest是restrict指针的信息必须传播memmove到最终转换为or 的循环memcpy,并且该信息必须在转换发生之前传播。您可以轻松地将循环转换为memmove,然后再找出参数是restrict,但为时已晚!
不知何故,当函数内联时,看起来src和 的信息dest会丢失。restrict这为我们提供了几种不同的理论来解释为什么会发生这种情况:
restrict也许由于错误,内联后的传播会以某种方式被破坏。
假设调用函数比被内联的函数有更多的上下文, GCC 可能会restrict在内联后从调用函数中进行推断。
也许优化过程没有以正确的顺序发生,无法restrict传播到循环。也许该信息会传播,然后执行内联,然后再进行循环优化。
毕竟,优化过程(代码转换过程)对重新排序很敏感。这是编译器设计中极其复杂的领域。
使用-fno-tree-loop-distribute-patterns, 或使用编译指示:
mymemcpy:
test edx, edx
je .L1
mov edx, edx
jmp memcpy
.L1:
ret
test:
test edx, edx
je .L4
mov edx, edx
jmp memmove
.L4:
ret
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
634 次 |
| 最近记录: |