为什么我的 C 代码调用 memmove (而不是 memcpy)

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)

Die*_*Epp 5

准确的答案需要深入研究 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 编译器会执行以下操作:

  1. 预处理+标记化源文件,

  2. 解析创建 AST,

  3. 类型检查,

  4. 优化,

  5. 发出代码。

实际上,“优化”项是对代码的许多不同的传递,并且每个传递都以不同的方式修改代码。这些遍在编译期间的不同时间发生,并且某些优化遍可能发生多次。

特定优化过程发生的顺序会影响结果。如果先执行优化 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)