对于 x86-64,哪个更快,imm64 或 m64?
Which is faster, imm64 or m64 for x86-64?
经过大约100亿次测试,如果imm64
比AMD64的m64
快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;
}
那么,m64
或 imm64
哪个最快?
顺便说一下,我使用的是 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. See Table 9.1 for uop-cache stuff. See also other performance links in the x86 标签 wiki。
编译器通常选择生成带有 mov r64, imm64
的 64 位常量。 (相关:, but in practice those never come up for scalar integer because there's 。)
这通常是正确的选择,尽管在长 运行 循环中,您希望常量在缓存中保持热状态,从 .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 的微小序列时,周围的代码非常重要,尤其是当它们竞争不同的吞吐量资源时。
经过大约100亿次测试,如果imm64
比AMD64的m64
快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;
}
那么,m64
或 imm64
哪个最快?
顺便说一下,我使用的是 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. See Table 9.1 for uop-cache stuff. See also other performance links in the x86 标签 wiki。
编译器通常选择生成带有 mov r64, imm64
的 64 位常量。 (相关:
这通常是正确的选择,尽管在长 运行 循环中,您希望常量在缓存中保持热状态,从 .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 的微小序列时,周围的代码非常重要,尤其是当它们竞争不同的吞吐量资源时。