assembly - 对于 x86-64,imm64 和 m64 哪个更快?

标签 assembly optimization x86 x86-64 micro-optimization

经过大约 100 亿次测试,如果 AMD64 的 imm64m64 快 0.1 纳秒,则 m64 似乎更快,但是我实在不明白。下面代码中的val_ptr的地址本身不就是一个立即数吗?

# Text section
.section __TEXT,__text,regular,pure_instructions
# 64-bit code
.code64
# Intel syntax
.intel_syntax noprefix
# Target macOS High Sierra
.macosx_version_min 10,13,0

# Make those two test functions global for the C measurer
.globl _test1
.globl _test2

# Test 1, imm64
_test1:
  # Move the immediate value 0xDEADBEEFFEEDFACE to RAX (return value)
  movabs rax, 0xDEADBEEFFEEDFACE
  ret
# Test 2, m64
_test2:
  # Move from the RAM (val_ptr) to RAX (return value)
  mov rax, qword ptr [rip + val_ptr]
  ret
# Data section
.section __DATA,__data
val_ptr:
  .quad 0xDEADBEEFFEEDFACE

测量代码为:

#include <stdio.h>            // For printf
#include <stdlib.h>           // For EXIT_SUCCESS
#include <math.h>             // For fabs
#include <stdint.h>           // For uint64_t
#include <stddef.h>           // For size_t
#include <string.h>           // For memset
#include <mach/mach_time.h>   // For time stuff

#define FUNCTION_COUNT  2     // Number of functions to test
#define TEST_COUNT      0x10000000  // Number of times to test each function

// Type aliases
typedef uint64_t rettype_t;
typedef rettype_t(*function_t)();

// External test functions (defined in Assembly)
rettype_t test1();
rettype_t test2();

// Program entry point
int main() {

  // Time measurement stuff
  mach_timebase_info_data_t info;
  mach_timebase_info(&info);

  // Sums to divide by the test count to get average
  double sums[FUNCTION_COUNT];

  // Initialize sums to 0
  memset(&sums, 0, FUNCTION_COUNT * sizeof (double));

  // Functions to test
  function_t functions[FUNCTION_COUNT] = {test1, test2};

  // Useless results (should be 0xDEADBEEFFEEDFACE), but good to have
  rettype_t results[FUNCTION_COUNT];

  // Function loop, may get unrolled based on optimization level
  for (size_t test_fn = 0; test_fn < FUNCTION_COUNT; test_fn++) {
    // Test this MANY times
    for (size_t test_num = 0; test_num < TEST_COUNT; test_num++) {
      // Get the nanoseconds before the action
      double nanoseconds = mach_absolute_time();
      // Do the action
      results[test_fn] = functions[test_fn]();
      // Measure the time it took
      nanoseconds = mach_absolute_time() - nanoseconds;

      // Convert it to nanoseconds
      nanoseconds *= info.numer;
      nanoseconds /= info.denom;

      // Add the nanosecond count to the sum
      sums[test_fn] += nanoseconds;
    }
  }
  // Compute the average
  for (size_t i = 0; i < FUNCTION_COUNT; i++) {
    sums[i] /= TEST_COUNT;
  }

  if (FUNCTION_COUNT == 2) {
    // Print some fancy information
    printf("Test 1 took %f nanoseconds average.\n", sums[0]);
    printf("Test 2 took %f nanoseconds average.\n", sums[1]);
    printf("Test %d was faster, with %f nanoseconds difference\n", sums[0] < sums[1] ? 1 : 2, fabs(sums[0] - sums[1]));
  } else {
    // Else, just print something
    for (size_t fn_i = 0; fn_i < FUNCTION_COUNT; fn_i++) {
      printf("Test %zu took %f clock ticks average.\n", fn_i + 1, sums[fn_i]);
    }
  }

  // Everything went fine!
  return EXIT_SUCCESS;
}

那么,m64imm64 哪一个确实最快?

顺便说一下,我使用的是 Intel Core i7 Ivy Bridge 和 DDR3 RAM。我正在运行 macOS High Sierra。

编辑:我插入了 ret 指令,现在 imm64 速度更快。

最佳答案

您没有显示您测试的实际循环,也没有说明您如何测量时间。显然,您测量的是挂钟时间,而不是核心时钟周期(带有性能计数器)。因此,您的测量噪声源包括睿频/节能以及与另一个逻辑线程共享物理核心(在 i7 上)。


在英特尔 IvyBridge 上:

movabs rax, 0xDEADBEEFFEEDFACE是一个ALU指令

  • 采用 10 个字节的代码大小(这可能重要也可能不重要,具体取决于周围的代码)。
  • 对于任何 ALU 端口(p0、p1 或 p5)解码为 1 uop。 (最大吞吐量 = 每个时钟 3 个)
  • 在 uop 缓存中占用 2 个条目(因为是 64 位立即数),并需要 2 个周期从 uop 缓存中读取。 (因此,如果这是包含此内容的代码中的瓶颈,那么从循环缓冲区运行对于前端吞吐量来说是一个显着的优势)。

mov rax, [RIP + val_ptr]是一个负载

  • 占用 7 个字节(REX + 操作码 + modrm + rel32)
  • 对于任一加载端口(p2 或 p3)解码为 1 uop。 (最大吞吐量 = 每个时钟 2)
  • 适合 uop 缓存中的 1 个条目(无立即数和 32 或 32 小地址偏移量)。
  • 如果负载跨页面边界分割,运行速度会慢很多,即使在 Skylake 上也是如此。
  • 第一次可能会错过缓存。

来源:Agner Fog's microarch pdf and instruction tables 。有关 uop 缓存内容,请参阅表 9.1。另请参阅 中的其他性能链接标签维基。


编译器通常选择生成带有mov r64, imm64的64位常量。 。 (相关: What are the best instruction sequences to generate vector constants on the fly? ,但实际上这些永远不会出现在标量整数中,因为有 no short single-instruction way to get a 64-bit -1 。)

这通常是正确的选择,尽管在长时间运行的循环中,您希望常量在缓存中保持热状态,但从 .rodata 加载它可能是一个胜利。 。特别是如果这可以让你做类似 and rax, [constant] 的事情而不是movabs r8, imm64/and rax, r8 .

If your 64-bit constant is an address ,使用 RIP 相关 lea相反,如果可能的话。 lea rax, [rel my_symbol]在 NASM 语法中,lea my_symbol(%rip), %rax在 AT&T。


在考虑微小的 asm 序列时,周围的代码非常重要,特别是当它们竞争不同的吞吐量资源时。

关于assembly - 对于 x86-64,imm64 和 m64 哪个更快?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46433208/

相关文章:

c++ - 优化位操作以获取 NOT 值

performance - 如何有效利用 channel

assembly - 进入长模式

linux - 如何在程序集中创建具有动态指定文件路径的文件?

c - 将表指针从 C 传递到汇编函数

c++ - 用 64 位替换 32 位循环计数器会在 Intel CPU 上使用 _mm_popcnt_u64 引入疯狂的性能偏差

c - 在 x86 汇编中交换 2 个整数

c++ - 使用 Godbolt 进入标准库调用

mysql - 如何优化大表查询

c - 将x86代码从x64进程注入(inject)到x86进程中