上文调研了某个知名库的 memcpy 实现,本文将调研 Linux 内核的 memset 实现。不过本文的实际动机是讨论 x86 的 REP_GOOD、ERMS 和 FSRS 微架构特性。(虽然初衷是为了电子斗蛐蛐……)
caturra@BLUEPUNI:~$ cat /proc/cpuinfo | grep flags
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca
cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb
rdtscp lm constant_tsc rep_good 👈 nopl xtopology tsc_reliable nonstop_tsc
cpuid extd_apicid tsc_known_freq pni pclmulqdq ssse3 fma cx16 sse4_1 sse4_2 movbe
popcnt aes xsave avx f16c rdrand hypervisor lahf_lm cmp_legacy svm cr8_legacy abm
sse4a misalignsse 3dnowprefetch osvw topoext perfctr_core ssbd ibrs ibpb stibp
vmmcall fsgsbase bmi1 avx2 smep bmi2 erms 👈 invpcid rdseed adx smap clflushopt
clwb sha_ni xsaveopt xsavec xgetbv1 xsaves clzero xsaveerptr arat npt nrip_save
tsc_scale vmcb_clean flushbyasid decodeassists pausefilter pfthreshold
v_vmsave_vmload umip vaes vpclmulqdq rdpid fsrm 👈(fsrs 的兄弟)
由于内核要求使用标量指令而非 SIMD 向量指令,所以实现是相对简单的。选择 Linux 内核的版本也能比较好的体现 x86 微架构的发展历史。
NOTES:
- 名词表:ERMS = Enhanced rep movsb/stosb,FSRS = Fast Short rep stos。
- 本文可视为考古文。(patch 倒是近几年的)我认为这些属于吹毛求疵的知识,了解一下就好。
- 本文也可以用来和上文做对比。同样是
mem*
设计,上文是花里胡哨的软件优化,本文是短至一条指令的硬件优化。现在该用什么,战未来该用什么,这是严重的分歧点。 - 本文没有 benchamrk 环节。因为不同的微架构和访问模式会有不一致的性能表现。我要做跑分很容易,但是什么都代表不了。本文会提供官方性质的字面参考。
memset 背景板
Linux 内核的 memset 实现位于 ./arch/x86/lib/memset_64.S
。
// Linux 内核 v6.3 的实现(2023 年)
SYM_FUNC_START(__memset)
/*
* Some CPUs support enhanced REP MOVSB/STOSB feature. It is recommended
* to use it when possible. If not available, use fast string instructions.
*
* Otherwise, use original memset function.
*/
// 有两个替换选项:orig 版本和 erms 版本
// - 如果只有 REP_GOOD 特性,则走下方一堆指令
// - 如果有 ERMS 特性,则走 memset_erms 实现(略)
// - 什么都没有,那就走 memset_orig 实现(略)
ALTERNATIVE_2 "jmp memset_orig", "", X86_FEATURE_REP_GOOD, \
"jmp memset_erms", X86_FEATURE_ERMS
// REP_GOOD 版本
// Q+B 的 rep stos 实现,先处理大块,再处理小块
movq %rdi,%r9
movq %rdx,%rcx
andl $7,%edx
shrq $3,%rcx
/* expand byte value */
movzbl %sil,%esi
movabs $0x0101010101010101,%rax
imulq %rsi,%rax
// Q 版本
rep stosq
movl %edx,%ecx
// B 版本
rep stosb
movq %r9,%rax
RET
SYM_FUNC_END(__memset)
EXPORT_SYMBOL(__memset)
至少 2015 年起,直到 2023 年,Linux 内核长期使用以上选择路径:
- 如果只有 REP_GOOD 特性,则使用 Q+B 的 rep stos 实现。
- 如果支持 ERMS 特性,则使用 memset_erms 定制实现。
- 什么都没有的话,就使用 memset_orig 通用实现。
注意这里 ALTERNATIVE 宏是运行时替换汇编指令的。
// Linux 内核 v6.17 的实现(2025 年)
SYM_TYPED_FUNC_START(__memset)
ALTERNATIVE "jmp memset_orig", "", X86_FEATURE_FSRS
// 如果 CPU 具有 FSRS 特性则走这里
// 只有 B 版本的 rep stos 实现
movq %rdi,%r9
movb %sil,%al
movq %rdx,%rcx
rep stosb
movq %r9,%rax
RET
SYM_FUNC_END(__memset)
EXPORT_SYMBOL(__memset)
那个人在 2023 年动手优化实现后,直到现在 2025 年都是以上选择路径:只看是否支持 FSRS。
可以看出,微架构随时代发展有不同的指导价值。接下来直接考古 REP_GOOD、ERMS 和 FSRS 三个特性。
REP_GOOD
// 文件:arch/x86/include/asm/cpufeatures.h
/* Other features, Linux-defined mapping, word 3 */
#define X86_FEATURE_REP_GOOD ( 3*32+16) /* "rep_good" REP microcode works well */
需要注意 REP_GOOD 并不是硬件特性,这是 Linux 特有的,只是你刚好能在 /proc/cpuinfo 里找到。
历史发展路线:
- X86_FEATURE_K8_C 引入:在 commit 1da177e 之前。这是 Linux 内核最早的 git 记录(2.6.12 时代,2005 年),我不想再往前追查了。注意此时已经有 memset rep 优化。
- X86_FEATURE_K8_C 移除。认为这种字符串优化很老派,删了。
- X86_FEATURE_REP_GOOD 正式引入(2006 年):实质和 K8_C 相同,只是删掉后(Intel NetBurst)性能吃亏又加回来了。(同一人)
就本人能找到的 git 历史来说,REP_GOOD 字面上的引入时间是在 2006 年,目标架构是 AMD K8 (C 步进版本),实质上应该更早时间就已经支持。根据它的 K8 判断基准,Intel 处理器不会受益。但是这条 commit 185f3b9 表明 Intel 进入 64 位时代也提供 REP_GOOD 支持。谁先谁后还不好说。
NOTES:
- 只看微码而不是 REP_GOOD 标记,有内部人士透露 Intel 早在 1996 年就尝试优化 rep 实现。
- 总之这是相当的老黄历了,进入 64 位时代后,应该没有不支持 REP_GOOD 的道理。
ERMS
/* Intel-defined CPU features, CPUID level 0x00000007:0 (EBX), word 9 */
#define X86_FEATURE_ERMS ( 9*32+ 9) /* "erms" Enhanced REP MOVSB/STOSB instructions */
ERMS 是处理器提供的硬件特性。需要注意出于历史原因,硬件特性多是 Intel 特有的(Intel-defined)。不过这不意味着是独有的功能,比如 AVX2 也是 Intel-defined 的,友商可以做兼容……
3.7.6 Enhanced REP MOVSB and STOSB Operation
Beginning with processors based on Ivy Bridge microarchitecture, REP string operation using MOVSB and STOSB can provide both flexible and high-performance REP string operations for software in common situations like memory copy and set operations. Processors that provide enhanced MOVSB/STOSB oper- ations are enumerated by the CPUID feature flag: CPUID:(EAX=7H, ECX=0H):EBX.[bit 9] = 1.
来源:Intel® 64 and IA-32 Architectures Optimization Reference Manual
历史发展:
- X86_FEATURE_ERMS 引入(2011 年):基本是为 CPU 发布做准备。
- ivy bridge 架构正式提供 ERMS 特性(2012 年)。Intel 文档有写。
- ERMS 覆盖 REP_GOOD 能力:来自 commit 15b7ddc(2025 年),这更多是 Intel 处理历史遗留问题。
注意 X86_FEATURE_ERMS 只是纸面上的定义,内核不会主动设置,从 patch 的说明来看是 BIOS 提供选项(从而有用户开关,并且 ALTERNATIVE 仍可通过 CPUID 识别到这个 flag)。
这里并没有提到任何 AMD 相关的信息,因为它对于 ERMS 的支持非常混乱。下面详细介绍。
由于 AMD 的文档只能用悲剧来形容,所以我需要反查 CPUID 指令。既然已知 CPUID:(EAX=7H, ECX=0H):EBX.[bit 9] = 1
可以检测该功能,那么……
// 需要使用 C++20 编译,依赖于 InstLatx64 提供的 CPUID dump 文件
#include <filesystem>
#include <string>
#include <string_view>
#include <charconv>
#include <iostream>
#include <fstream>
#include <ranges>
#include <vector>
namespace stdf = std::filesystem;
namespace stdv = std::views;
namespace stdr = std::ranges;
using namespace std::literals;
// 执行时可指定要解析的目录,不指定就是当前目录
int main(int argc, char **argv) {
const stdf::path root {argc > 1 ? argv[1] : "."};
std::cout << "Start: " << root << std::endl;
auto cpuid_txt_path_view =
stdv::filter([](auto &&dentry) { return dentry.is_regular_file(); })
| stdv::transform([](auto &&dentry) { return dentry.path(); })
| stdv::filter([](auto &&path) { return path.extension() == ".txt"; })
| stdv::filter([](auto &&path) {
auto name = path.filename().string();
return name.find("CPUID"sv) != std::string::npos;
});
auto optimized_out = [](auto &&line) {
return line.starts_with("CPUID 80000001");
};
auto contains_feature = [](auto &&line) {
return line.starts_with("CPUID 00000007");
};
auto test_bit = [](auto &&line, auto bit_index) {
constexpr size_t bias = 25;
const char *features_hex {line.data() + bias};
unsigned long long bitmap = 0;
std::from_chars(features_hex, features_hex + 8, bitmap, 16);
return bitmap >> bit_index & 1;
};
std::vector<stdf::path> ok, ng;
for(auto &&path : stdf::recursive_directory_iterator(root) | cpuid_txt_path_view) {
std::ifstream file_stream {path};
std::string line;
while(std::getline(file_stream, line)) {
if(optimized_out(line)) break;
if(!contains_feature(line)) continue;
auto &vec = test_bit(line, 9 /* ERMS */) ? ok : ng;
vec.emplace_back(path);
break;
}
}
std::cout << "OK list:" << std::endl;
for(auto &&path : ok) {
std::cout << " " << path.filename() << std::endl;
}
std::cout << std::endl;
std::cout << "NG list:" << std::endl;
for(auto &&path : ng) {
std::cout << " " << path.filename() << std::endl;
}
}
你可以浏览别人做好的 CPUID dump 文件来确认历代特性支持信息。我使用上面的程序枚举出最早支持的是 Vermeer 架构(2020 年 / Zen 3),基本是落后 Intel 十年了。
NOTE:commit ca96b16 提到直到 2023 年也有部分 EPYC 服务器不支持 ERMS。
FSRM(番外)
同样 FSRM 也是 Intel 定义的。需要注意,FSRM 和 FSRS 是不一样的功能,前者是 move 操作,后者是 store 操作。
所以 FSRM 不会优化 memset 的性能(用不到这个指令),但是可以优化 memmove/memcpy。
3.7.6.1 Fast Short REP MOVSB
Beginning with processors based on Ice Lake Client microarchitecture, REP MOVSB performance of short operations is enhanced. The enhancement applies to string lengths between 1 and 128 bytes long. Support for fast-short REP MOVSB is enumerated by the CPUID feature flag: CPUID [EAX=7H, ECX=0H).EDX.FAST_SHORT_REP_MOVSB[bit 4] = 1. There is no change in the REP STOS performance.
来源:Intel® 64 and IA-32 Architectures Optimization Reference Manual
Intel 在优化手册中提到,Ice Lake(2019 年)后支持 FSRM。那 AMD 呢?当然是什么都没说。
NOTES:
- 关于 move 操作的硬件优化,还有 FZRM 特性(Fast Zero Length REP MOVSB),感兴趣请自行看 Intel 手册。
- 如果你使用谷歌搜索 AMD 相关的信息,那么大概率会看到有人在抱怨 Zen3 对 FSRM 支持不佳。
- 2020 年,Linux 内核正式支持 FSRM,并且第一步就是改进 memmove 实现。
FSRS
历史发展:
Support for fast-short REP STOSB is enumerated by the CPUID feature flag:
CPUID.07H.01H:EAX.FAST_SHORT_REP_STOSB[bit 11] = 1.
稍微改动上面 ERMS 的枚举程序得知:
- Intel 从 Alder Lake(2021 年)开始均支持 FSRS。
- AMD 全军覆没,直到 Zen 5(2024-2025 年)都不支持 FSRS。
这可能说明,那个人所做的修改,对于 AMD 来说是吃亏的,因为它直接回落到最通用的基本实现了。
那怎么办?那个人又出手了:只要 AMD 支持 FSRM,那就等同于支持 FSRS。所以对于 Linux 内核来说,这不是问题。
纸面斗蛐蛐
TODO. 收了点数据,但是太花时间,不是很想写了。
下面是草稿,我不想更新的时候自行查阅吧。
3.8.2 Fast Short REP STOSB REP STOSB performance of short operations is enhanced. The enhancement applies to string lengths between 0 and 128 bytes long. When Fast Short REP STOSB feature is enabled, REP STOSB performance is flat 12 cycles per operation, for all strings 0-128 byte long whose destination operand resides in the processor first level cache.
ZEN 5 手册是唯一特意强调有极佳优化 rep(你能想到的软件优化,AMD 都帮你在微码上做好了),这是此前所有 AMD 优化手册都没提到的事情。
agner 汇编指南 2025 认为 rep 是战未来的好指令,因为 AVX512 变强它也同样受益,但是 rep 有独有的 code size 优势。
Intel 优化手册比过旧时代的 SIMD/rep 性能:ERMS时期,rep stosb,可以比d+b有潜在的吞吐提升。但是 ivy 明确提到可能不如 mm256。
Intel 为 FSRS 定性到 128 范围内,个人推测是因为此前 rep 这个范围在 haswell 时期被 mm128 超越了(见优化指南,但是 > 128 总是 rep 小胜)。
msvc/gcc 提到过对齐回退问题
TO BE CONTINUED
⬅️ TO BE CONTINUED