为什么动态链接的可执行文件明显比 Linux 中的静态链接的慢?

Why is a dynamically linked executable noticeably slower than the statically linked one in Linux?

用确定井字棋盘结果的玩具程序进行测试,我明白了。是什么造成了如此大的差异?我怀疑使用静态链接的 libc 调用 rand 会更快,但仍然对结果感到惊讶。

~$ gcc a.c -std=c11 -O3
~$ time ./a.out
32614644

real    0m9.396s
user    0m9.388s
sys     0m0.004s

~$ gcc a.c -std=c11 -O3 -static
~$ time ./a.out
32614644

real    0m6.891s
user    0m6.884s
sys     0m0.000s

#include <stdio.h>
#include <stdlib.h>

#define SIZE 3
#define SIZE_2 (SIZE * SIZE)

static int determineResult(int board[static SIZE_2]) {
  for (int i = 0; i < SIZE_2; i += SIZE) {
    if (!board[i]) {
      continue;
    }
    for (int j = i + 1; j < i + SIZE; ++j) {
      if (board[i] != board[j]) {
        goto next;
      }
    }
    return board[i];
  next:;
  }
  for (int i = 0; i < SIZE; ++i) {
    if (!board[i]) {
      continue;
    }
    for (int j = i + SIZE; j < i + SIZE_2; j += SIZE) {
      if (board[i] != board[j]) {
        goto next2;
      }
    }
    return board[i];
  next2:;
  }
  for (int i = SIZE + 1; i < SIZE_2; i += SIZE + 1) {
    if (board[i] != *board) {
      goto next3;
    }
  }
  return *board;
next3:
  for (int i = SIZE * 2 - 2; i <= SIZE_2 - SIZE; i += SIZE - 1) {
    if (board[i] != board[SIZE - 1]) {
      return 0;
    }
  }
  return board[SIZE - 1];
}

#define N 50000000

int main(void) {
  srand(0);
  size_t n = 0;
  for (int i = 0; i < N; ++i) {
    int board[SIZE_2];
    for (int i = 0; i < SIZE_2; ++i) {
      board[i] = rand() % 3;
    }
    n += determineResult(board);
  }
  printf("%zu\n", n);
  return EXIT_SUCCESS;
}

这里的问题是您比较的是总执行时间,在像这样的小示例中,静态链接示例要好得多,因为不需要进行查找。

静态链接和动态链接的最大区别在于动态链接有多个 modules/objects 在运行时链接在一起,而静态编译的二进制文件包含二进制文件中的所有内容。当然,有些细节可能会有所不同,但大致就是这样。

所以...考虑到上述因素,启动一个可执行文件、加载几个不同的文件、执行您的函数并返回可能比加载文件和执行您的函数花费更多的时间。

在不知道您的系统使用的特定 ABI(取决于 OS 和 cpu 架构)的情况下,我无法确定这是原因,但以下是最可能的解释.

在大多数实现中,共享库(包括共享libc.so)中的代码必须是位置无关代码。这意味着它可以在任何地址加载(而不是由链接器分配一个固定的 运行 时间地址),因此不能在机器代码中使用硬编码的绝对数据地址。相反,它必须通过指令指针相对寻址或 全局偏移量 table (GOT) 访问全局数据,其地址要么保存在寄存器中,要么相对于指令指针。这些寻址模式主要在设计良好的现代指令集体系结构(如 x86_64、AArch64、RISC-V 等)上有效。在大多数其他体系结构(包括 32 位 x86)上,它们的效率非常低。例如下面的函数:

int x;
int get_x()
{
    return x;
}

在 x86 上会像下面这样膨胀:

get_x:
    push %ebp
    mov %esp, %ebp
    push %ebx
    sub , %esp
    call __x86.get_pc_thunk_bx
    add $_GLOBAL_OFFSET_TABLE_, %ebx
    mov x@GOT(%ebx), %eax
    mov (%eax),%eax
    add , %esp
    pop %ebx
    pop %ebp
    ret

而您希望(对于非位置无关代码)看到:

get_x:
    mov x, %eax
    ret

由于随机数生成器具有内部(全局)状态,因此他们不得不为与位置无关的代码执行这种昂贵的舞蹈。由于他们所做的实际计算可能非常短且快速,因此 PIC 开销可能是他们 运行 时间的重要部分。

证实这一理论的一种方法是尝试使用 rand_rrandom_r。这些函数使用调用者提供的状态,因此可以(至少在理论上)避免对全局数据的任何内部访问。