公众号

点击「查看原文」跳转到 GitHub 上对应文件,链接就可以点击了
qq群 753792291 答疑在这里
欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言
本期文章由 赞助老爷 赞助 在此表示感谢
标准委员会动态/ide/编译器信息放在这里
编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期
这篇文章讲解如何用安全的range-based循环逐步替代容易出错的索引循环。
传统for循环的陷阱:
for (auto i = 0; i <= vec.size(); ++i) // 应该是 <,不是 <=
use(vec[i]);
基本range-for(C++11):
for (Record const& rec : records)
use(rec);
反向迭代(C++20):
using std::views::reverse;
for (Record const& rec : reverse(records))
use(rec);
同时迭代两个序列(C++23):
using std::views::zip;
for (auto [name, rec] : zip(names, records))
use(name, rec);
带索引的迭代(C++20+):
using std::views::iota;
using std::views::zip;
for (auto [i, rec] : zip(iota(0), records))
use(i, rec);
简化的索引迭代(C++23):
using std::views::enumerate;
for (auto [i, rec] : enumerate(records))
use(i, rec);
需要传统循环的场景:
// 复杂索引计算
for (int i = 0; i != records.size(); i = records[i])
use(records[i]);
能用range就别手写索引。
文章讨论了从C++11前基于struct的functor到现代lambda方案的演进,提倡”functor工厂”——返回lambda的函数。
传统Functor Struct(C++11前):
struct key_less {
bool operator()(T const& lhs, T const& rhs) const {
return lhs.key < rhs.key;
}
};
std::sort(c.begin(), c.end(), key_less{});
带上下文的Functor:
struct indirect_less {
indirect_less(std::vector<int> const& indirection)
: indirection_(indirection) {}
bool operator()(T const& lhs, T const& rhs) const {
return indirection_[lhs.key] < indirection_[rhs.key];
}
private:
std::vector<int> const& indirection_;
};
现代Lambda方式:
auto indirect_less = [&](T const& lhs, T const& rhs) {
return indirection[lhs.key] < indirection[rhs.key];
};
推荐的Lambda工厂模式:
auto make_indirect_less(std::vector const& indirection) {
return [&](T const& lhs, T const& rhs) {
return indirection[lhs.key] < indirection[rhs.key];
};
}
Victor Zverovich展示了新的浮点转字符串算法zmij,显著超越现有方法,原则是”不做比做点什么更快”。
性能基准:
std::to_chars (libc++)快3.5倍sprintf快59倍单个double转换:Apple M1上约10-20纳秒。
关键代码 - 优化的对数近似:
constexpr int log10_2_sig = 315'653;
constexpr int log10_2_exp = 20;
auto floor_log10_pow2(int e) noexcept -> int {
return e * log10_2_sig >> log10_2_exp;
}
快速除法和取模:
inline auto divmod100(uint32_t value) noexcept -> divmod_result {
assert(value < 10'000);
constexpr int exp = 19;
constexpr int sig = (1 << exp) / 100 + 1;
uint32_t div = (value * sig) >> exp;
return {div, value - div * 100};
}
主要改进:
应用:
std::to_string代码仓库:https://github.com/vitaut/zmij
这篇文章探讨如何用Intel的vpternlog{d,q}指令——三操作数位逻辑运算——在x86-64处理器上实现32位和64位有符号饱和运算。
vpternlog指令:“三个操作数的位三元逻辑,能实现任何三操作数布尔函数”,通过8位查找表索引三个输入操作数的位。
饱和运算:防止溢出/下溢回绕,把结果钳位到最大值(0x7FFFFFFF)或最小值(0x80000000)。
检测逻辑:
加法,输入符号相同但结果符号不同时饱和:
0b01000010 (0x42)~(a ^ b) & (a ^ c)减法,输入符号不同且结果符号与第一操作数不同时饱和:
0b00011000 (0x18)(a ^ b) & (a ^ c)32位有符号饱和加法:
__m128i _mm_adds_epi32(__m128i a, __m128i b)
{
__m128i Result = _mm_add_epi32(a, b);
const std::uint32_t Lut = 0b01000010;
const __m128i SaturationCheck = _mm_ternarylogic_epi32(a, b, Result, Lut);
const __mmask8 SaturationMask = _mm_movepi32_mask(SaturationCheck);
Result = _mm_mask_srai_epi32(Result, SaturationMask, Result, 31);
Result = _mm_mask_xor_epi32(
Result, SaturationMask, Result, _mm_set1_epi32(0x80000000u)
);
return Result;
}
32位有符号饱和减法:
__m128i _mm_subs_epi32(__m128i a, __m128i b)
{
__m128i Result = _mm_sub_epi32(a, b);
const std::uint32_t Lut = 0b00011000;
const __m128i SaturationCheck = _mm_ternarylogic_epi32(a, b, Result, Lut);
const __mmask8 SaturationMask = _mm_movepi32_mask(SaturationCheck);
Result = _mm_mask_srai_epi32(Result, SaturationMask, Result, 31);
Result = _mm_mask_xor_epi32(
Result, SaturationMask, Result, _mm_set1_epi32(0x80000000u)
);
return Result;
}
64位有符号饱和加法:
__m128i _mm_adds_epi64(__m128i a, __m128i b)
{
__m128i Result = _mm_add_epi64(a, b);
const __m128i SaturationCheck = _mm_ternarylogic_epi64(a, b, Result, 0b01000010);
const __mmask8 SaturationMask = _mm_movepi64_mask(SaturationCheck);
Result = _mm_mask_srai_epi64(Result, SaturationMask, Result, 63);
Result = _mm_mask_xor_epi64(
Result, SaturationMask, Result, _mm_set1_epi64x(0x8000000000000000)
);
return Result;
}
64位有符号饱和减法:
__m128i _mm_subs_epi64(__m128i a, __m128i b)
{
__m128i Result = _mm_sub_epi64(a, b);
const __m128i SaturationCheck = _mm_ternarylogic_epi64(a, b, Result, 0b00011000);
const __mmask8 SaturationMask = _mm_movepi64_mask(SaturationCheck);
Result = _mm_mask_srai_epi64(Result, SaturationMask, Result, 63);
Result = _mm_mask_xor_epi64(
Result, SaturationMask, Result, _mm_set1_epi64x(0x8000000000000000)
);
return Result;
}
算法总结:
vpternlog根据符号位检测溢出/下溢vpmov{d,q}2m将检测结果转换为执行掩码Marius Bancila探讨C++中高效矩阵乘法的各种实现策略,回应Python声称在此任务上优于C++的病毒式说法。文章对比了1000×1000矩阵的多种实现。
数据设置:
constexpr std::size_t N = 1000;
std::vector<double> A_data(N * N);
std::vector<double> B_data(N * N);
std::vector<double> C_data(N * N);
std::random_device rd{};
std::mt19937 rng(rd());
std::uniform_real_distribution<double> dist(-1000, 1000);
for (auto& v : A_data) v = dist(rng);
for (auto& v : B_data) v = dist(rng);
朴素实现(~850ms):
void naive_multiplication(std::vector<double> const & A,
std::vector<double> const & B,
std::vector<double> & C,
std::size_t const N)
{
for (std::size_t i = 0; i < N; ++i) {
for (std::size_t j = 0; j < N; ++j) {
C[i * N + j] = 0;
for (std::size_t k = 0; k < N; ++k) {
C[i * N + j] += A[i * N + k] * B[k * N + j];
}
}
}
}
优化的朴素实现(~700ms):
void naive_optimized_multiplication(std::vector<double> const& A,
std::vector<double> const& B,
std::vector<double>& C,
std::size_t const N)
{
for (std::size_t i = 0; i < N; ++i) {
for (std::size_t j = 0; j < N; ++j) {
double sum = 0;
for (std::size_t k = 0; k < N; ++k) {
sum += A[i * N + k] * B[k * N + j];
}
C[i * N + j] = sum;
}
}
}
关键优化:”用独立变量计算和而不是每次乘法后写入C矩阵”,通过寄存器存储提升约15%。
并行化版本(~170ms):
void parallelized_multiplication(std::vector<double> const& A,
std::vector<double> const& B,
std::vector<double>& C,
std::size_t const N)
{
std::vector<std::size_t> rows(N, 0);
std::for_each(
std::execution::par,
rows.begin(), rows.end(),
[&](std::size_t i) {
for (std::size_t j = 0; j < N; ++j) {
double sum = 0;
for (std::size_t k = 0; k < N; ++k)
sum += A[i * N + k] * B[k * N + j];
C[i * N + j] = sum;
}
});
}
性能:比朴素方法快5倍。
OpenBLAS库(~10ms):
#pragma comment(lib,"libopenblas.lib")
#include "cblas.h"
void openblas_multiplication(std::vector<double> const& A,
std::vector<double> const& B,
std::vector<double>& C,
blasint const N)
{
cblas_dgemm(
CblasRowMajor,
CblasNoTrans,
CblasNoTrans,
N, N, N,
1.0,
A.data(), N,
B.data(), N,
0.0,
C.data(), N
);
}
性能:比朴素方法快80倍;比并行化快15倍。
C++26标准方法(概念):
#include <linalg>
#include <mdspan>
#include <vector>
void linalg_multiplication(std::vector<double> const& A,
std::vector<double> const& B,
std::vector<double>& C,
std::size_t const N)
{
std::mdspan A_view(A.data(), N, N);
std::mdspan B_view(B.data(), N, N);
std::mdspan C_view(C.data(), N, N);
std::linalg::matrix_product(A_view, B_view, C_view);
}
别自己写矩阵乘法了,用OpenBLAS或等C++26的linalg。
Loop Unswitching咋翻译?
核心概念:
编译器识别出循环内的条件在执行期间永不改变。与其重复评估该条件,不如为每个可能分支创建两个专用循环版本。”编译器意识到bool squared值在整个循环中不变,决定复制循环。”
原始代码示例:
int sum_values(std::vector<int> const& values, bool squared) {
int sum = 0;
for (int value : values) {
sum += squared ? (value * value) : value;
}
return sum;
}
在-O2优化级别:
编译器展开三元运算符但保持单一循环:
// 编译器生成的等价代码(伪代码)
for (int value : values) {
sum += value * (squared ? value : 1); // 每次循环都判断
}
在-O3或更高优化级别(Loop Unswitching后):
编译器将循环复制成两个版本:
// 编译器生成的等价代码(伪代码)
if (squared) {
// 专用于平方的循环
for (int value : values) {
sum += value * value; // 无条件判断
}
} else {
// 专用于不平方的循环
for (int value : values) {
sum += value; // 无乘法运算
}
}
生成的汇编示例(概念展示):
; squared == true 分支
.LBB0_4:
mov eax, [rsi]
imul eax, eax ; value * value
add edi, eax ; sum += result
add rsi, 4
cmp rsi, rdx
jne .LBB0_4
; squared == false 分支
.LBB0_2:
mov eax, [rsi]
add edi, eax ; sum += value (无乘法)
add rsi, 4
cmp rsi, rdx
jne .LBB0_2
权衡:
好处:
代价:
“信任编译器的决策是好的,但要知道它在各种优化级别做的权衡。”可以用Compiler Explorer验证实际行为。
循环内的常量条件会被编译器提到外面,复制循环消除重复判断。代码变大但更快。
Matt Godbolt探讨现代编译器如何优化除法操作——通过转换成乘法和位移,完全消除昂贵的除法指令。
代码示例:
文章展示了转换无符号整数为ASCII十进制表示的带注释汇编:
to_decimal_backwards(unsigned int, char*):
mov rax, rsi ; rax = buf
mov esi, 3435973837 ; esi = 0xcccccccd
.L2:
mov edx, edi ; edx = number
mov ecx, edi ; ecx = number
add rax, 1 ; ++buf
imul rdx, rsi ; rdx *= 0xcccccccd
shr rdx, 35 ; rdx = rdx >> 35
; rdx = number / 10
lea r8d, [rdx+rdx*4] ; r8 = rdx * 5
add r8d, r8d ; r8 = rdx * 10
sub ecx, r8d ; ecx = number % 10
add ecx, 48 ; ecx = '0' + digit
cmp edi, 9 ; number > 9?
mov edi, edx ; number = number / 10
mov BYTE PTR [rax-1], cl ; store ASCII digit
ja .L2 ; loop if needed
ret
关键优化技术:
编译器用魔数常量0xcccccccd乘法后右移35位替代除以10。这有效是因为”该常量与2³⁵的比率近似十分之一,对所有无符号整数值有足够精度。”
编译器把除法变乘法加移位,魔数0xcccccccd实现除以10。
Matt Godbolt探讨不同C++循环风格是否影响性能,演示现代编译器将各种循环方法优化成相同汇编代码。
**三种循环实现对比(对vector
1. 基于序数的循环:
for (int i = 0; i < vec.size(); ++i) {
sum += vec[i];
}
2. 基于指针的while循环:
int* start = vec.data();
int* end = start + vec.size();
while (start != end) {
sum += *start;
++start;
}
3. Range-for循环:
for (int val : vec) {
sum += val;
}
关键发现:
编译器将所有三种方法转换成相同汇编。作者声称:”无论写显式循环还是用标准算法,编译器都能看穿到底层迭代模式。”Range-for和基于指针的方法产生比基于索引的迭代更优代码,因为消除了不必要的大小计算。
写清晰的、意图明确的代码,现代优化器会处理性能细节。Range-for、指针、索引循环编译结果一样,写最清楚的就行。
Matt Godbolt探讨编译器如何通过归纳变量转换优化循环。关键洞察是编译器通常比手动”优化”表现更好,通过策略性维护循环携带依赖。
优化的累加器方法(归纳变量)汇编:
.L3:
add rdx, 4 ; advance output pointer
mov DWORD PTR [rdx-4], eax; write accumulator
add eax, edi ; add 'table' to accumulator
cmp rdx, rcx ; reached end?
jne .L3 ; loop if not
关键洞察:
“编译器是智能的——信任它们。但知道如何用Compiler Explorer和llvm-mca这样的工具验证,并始终基准测试。”
归纳变量把乘法变加法,但手动优化可能因循环依赖反而更慢。信编译器。
Matt Godbolt探讨循环不变代码外提(LICM)——编译器优化,当代码不依赖循环迭代时将其移出循环外。
核心概念:
“这样的转换叫循环不变代码外提,或LICM。不仅是for子句本身的表达式,编译器能证明不依赖于哪次迭代的循环内任何代码都是公平游戏。”
代码示例(概念):
// 朴素实现
// get_range()在循环中重复调用
for (char c : string_view) {
auto range = get_range(); // 不变 - 被移出
if (c >= range.first && c <= range.second) count++;
}
优化后的汇编:
Clang成功将get_range()调用提到循环外:
call get_range(RangeType)@PLT ; 在循环外
movsx ecx, al
shr eax, 8
movsx edx, al
; ... 循环体使用缓存的range值
关键洞察:
setle、setge)消除循环内分支作者指出”gcc似乎无法利用这个优化”,提交bug建议问题与结构化返回类型的公共子表达式消除限制相关。
LICM把循环里不变的代码提到外面,但不是所有编译器都聪明,GCC就没做好。
Matt Godbolt探讨循环不变代码外提(LICM)优化如何因某些代码模式消失,特别检查char*指针的别名问题。
示例1:LICM正常工作
文章引用了检查字符串中感叹号的函数,最初编译成高效汇编,其中strlen被提出循环外。
示例2:LICM工作的汇编:
.L4:
add rdi, 1 ; ++index
cmp BYTE PTR [rdi-1], 33 ; is string[index-1] a '!' ?
je .L5 ; if so, jump to "return true"
.L2:
cmp rdi, rax ; else...are we at the end?
jne .L4 ; loop if not
示例3:添加instrumentation后LICM被破坏:
.L4:
add QWORD PTR num_compares[rip], 1; ++num_compares;
cmp BYTE PTR [rbp+0+rbx], 33 ; is char a '!' ?
je .L5 ; if so, jump to "return true"
add rbx, 1 ; ++index
.L2:
mov rdi, rbp ; er...
call strlen ; what the heck? strlen?
cmp rbx, rax ; oh no! what happened?
jb .L4 ; loop if index != strlen(...)
关键问题:
添加全局计数器(num_compares)导致编译器失去LICM优化。原因:”编译器无法证明我们获取长度的字符串不与num_compares变量共享内存。”
根本原因:
按C++标准,char*有特殊别名属性——可能合法地与任何其他类型别名。这防止编译器安全假设字符串和独立变量不在内存中重叠,强制在循环内重新计算strlen()。
char*能和任何类型别名,编译器不敢假设字符串和其他变量不重叠,LICM失效。
Matt Godbolt的博文解释别名如何阻止编译器优化,通过在循环中强制不必要的内存操作。
int版本(劣化):
mov eax, DWORD PTR [rdi] ; eax = total
.L3:
add eax, DWORD PTR [rsi] ; add element to total
add rsi, 4 ; move to next element
mov DWORD PTR [rdi], eax ; total = eax
cmp rsi, rdx ; are we finished?
jne .L3 ; loop if not
ret
long版本(优化):
mov rax, QWORD PTR [rdi] ; rax = total
.L9:
movsx rdx, DWORD PTR [rsi] ; read & sign extend next element
add rsi, 4 ; move to next element
add rax, rdx ; rax += element
cmp rcx, rsi ; are we finished?
jne .L9 ; loop if not
mov QWORD PTR [rdi], rax ; total = rax
ret
关键洞察:
int类型,编译器必须每次迭代更新内存,因为span参数可能与成员变量别名int vs long)触发严格别名规则,允许编译器在寄存器中缓存值std::accumulate()的局部变量或应用非标准__restrict限定符承诺唯一指针访问文章强调:”别名是C++更隐蔽的陷阱之一…你无法避免使用它们。”
类型相同可能别名,编译器不敢优化。用不同类型或__restrict能帮上忙。
Matt Godbolt解释调用约定——管理x86 Linux上函数参数如何传递的ABI规则。演示编译器如何基于数据类型和参数数量优化参数传递。
System V ABI基础:
“前几个参数放在rdi和rsi“用于整数类型和指针。
结构打包:
当传递包含两个long值的结构时,编译器高效地将两者打包进寄存器rdi和rsi,生成与独立参数版本相同的汇编。
寄存器溢出示例(int类型):
; structure is _packed_ into rdi as "y<<32 | x"
mov rax, rdi ; rax = args.y<<32 | args.x
shr rax, 32 ; rax >>= 32; rax is now 'y'
add eax, edi ; y += x;
ret
编译器策略性地使用64位和32位寄存器名高效处理打包数据。
多参数传递:
只有前6个参数适合寄存器;额外参数通过像add rax, QWORD PTR [rsp+8]这样的指令溢出到栈。
实用启示:
理解调用约定影响像std::string_view和std::optional这样类型的设计决策,帮助开发者优化参数传递并实现更好的编译器优化。
前6个参数放寄存器,超出的上栈。结构能打包进寄存器。懂调用约定能写出编译器友好的代码。
Matt Godbolt解释内联——用函数体替换函数调用——根本上重要不是为了消除调用开销,而是为了启用后续编译器优化。
内联启用什么:
示例:字符串大小写转换
文章用change_case工具函数演示。当内联到make_upper并upper总是true时,编译器消除未使用的代码路径并生成优化的ARMv7汇编:
.LBB0_1:
ldrb r2, [r0] ; read next character
sub r3, r2, #97 ; subtract 'a'
uxtb r3, r3 ; unsigned extend byte
cmp r3, #26 ; compare against 26
sublo r2, r2, #32 ; conditionally subtract 32
strb r2, [r0], #1 ; store result back
subs r1, r1, #1 ; decrement counter
bne .LBB0_1 ; loop
权衡:
缺点:代码膨胀会压迫指令缓存。内联决策依赖”启发式驱动”的编译器猜测,当代码变化时创建不可预测的涓滴效应。
可见性要求:编译器需要函数定义,不只是声明。链接时优化(LTO)帮助跨翻译单元内联。
内联把通用函数体转换成可优化的、特化的代码副本——使其成为现代编译器的”终极启用优化”。
内联不是为了省调用开销,是为了让编译器能做常量传播和死代码消除。代价是代码膨胀。
Matt Godbolt解释部分内联,一种编译器优化技术,通过将函数分成热路径和冷路径来平衡性能与代码大小。
核心概念:
“编译器将process分成两个函数,一个process (part.0)只做昂贵的部分。”技术涉及函数外联后跟选择性内联。
外联后的原始包装器:
process(unsigned int):
cmp edi, 99
jbe .L7
jmp process(unsigned int) (.part.0)
.L7:
lea eax, [rdi+rdi]
ret
内联的compute函数:
compute(unsigned int, unsigned int):
cmp edi, 99
jbe .L13
call process(unsigned int) (.part.0)
mov r8d, eax
cmp esi, 99
jbe .L14
.L11:
mov edi, esi
call process(unsigned int) (.part.0)
add eax, r8d
ret
.L13:
lea r8d, [rdi+rdi]
cmp esi, 99
ja .L11
.L14:
lea eax, [rsi+rsi]
add eax, r8d
ret
好处:
这种方法实现”两全其美:我们获得内联轻量检查和简单算术的性能好处,同时避免复制昂贵计算导致的代码膨胀。”
部分内联把函数拆成快路径和慢路径,快路径内联,慢路径调用。性能和代码大小都照顾到了。
基础前向声明:
class Cat;
class Dog {
Cat* garfield{};
};
危险的delete模式:
struct Cat;
void Fun(Cat* garfield) {
delete garfield; // 未定义行为!
}
struct Cat {
~Cat() { puts("Cats have seven lives!"); }
};
问题:删除不完整类型时,”析构函数和类特定的operator delete都不会被调用”,导致未定义行为。编译器对不完整类型假设析构函数是平凡的。
安全删除用static_assert:
void Fun(Cat* garfield) {
static_assert(sizeof(Cat) >= 0, "Cannot delete an incomplete type");
delete garfield;
}
好处:
安全措施:
sizeof()配合static_assert强制完整类型std::unique_ptr已经验证完整性前向声明省编译时间,但得小心delete。用static_assert保平安。
Bruce Dawson通过一个不寻常的观察发现泄漏:同事系统上的进程ID有七位数。因为Windows PID是4的倍数且快速复用,”四百万左右的PID意味着一百万个进程”,说明是进程句柄泄漏而不是真的进程泄漏。
根本原因:
vscode-windows-process-tree的buggy代码调用OpenProcess()但没对应的CloseHandle()。每个未关闭的句柄泄漏大约64 KB,最终消耗”大约64 GB”内存,没有上限。
调查技术:
修复:
加一行解决:CloseHandle(hProcess);
关键洞察:
Dawson主张用RAII模式防止这类泄漏,建议自动资源限制能在测试阶段而不是生产环境抓到这些bug。
基础range迭代:
迭代optional,执行零次或一次:
void doSomething(std::string const& data, std::optional<Logger&> logger = {}) {
for (auto l : logger) {
l.log(data);
}
return;
}
实际用例:与range流水线链式:
std::unordered_set<int> s{1, 3, 7, 9};
const auto flt = [&](int i) -> std::optional<int> {
if (s.contains(i)) {
return i;
} else {
return {};
}
};
for (auto i : std::views::iota(1, 10) | std::views::transform(flt)) {
for (auto j : i) {
for (auto k : std::views::iota(0, j)) {
std::ignore = k;
}
}
}
关键设计决策:
std::optional<T>现在简单地特化ranges::enable_view而不是继承view_interface,保持简单性的同时获得range能力。
optional能直接用在range pipeline里,挺方便。
std::ranges方式(函数式):
auto even_numbers = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::ranges::to<std::vector>();
字符串trim用std::ranges:
s | std::views::drop_while(is_space)
| std::views::reverse
| std::views::drop_while(is_space)
| std::views::reverse;
传统方式(命令式):
while (!input.empty() && is_space(input.front())) {
input.remove_prefix(1);
}
while (!input.empty() && is_space(input.back())) {
input.remove_suffix(1);
}
性能基准测试结果:
随机无空格字符串,每字符串处理的指令数:
| 实现方式 | LLVM 17/Apple M4 | GCC 15/Intel IceLake |
|---|---|---|
| std::ranges | 24条指令 | 70条指令 |
| 传统方式 | 18条指令 | 16条指令 |
关键发现:传统方式在两种编译器和处理器组合上都生成更少的指令,说明函数式抽象不自动保证最优性能。
ranges写着优雅,跑着不一定快。别盲目信抽象。
核心解决方案:deferred_call工具:
template<typename F>
struct deferred_call
{
using result_type=decltype(std::declval<const F>()());
operator result_type() const { return f(); }
F f;
};
工作原理:deferred_call对象传给接受泛型、无约束参数的函数/构造函数模板,转换只在实际需要值时触发。
应用示例:
object* retrieve_or_create(int id)
{
static std::unordered_map<int, std::unique_ptr<object>> m;
auto [it,b] = m.try_emplace(
id,
deferred_call([&]{ return std::make_unique<object>(id); }));
return it->second.get();
}
这延迟对象构造,直到try_emplace确认没有等价条目存在。
关键要求:
技术要求恰好一个用户定义转换点,否则链式转换失败:
void f(std::string);
// 错误:需要 deferred_call → const char* → std::string
f(deferred_call([]{ return "hello"; }));
延迟求值省开销,但转换链要小心。
Boost.Unordered 1.80三大优化:
内存开销对比(64位):
| 实现 | 开销公式 |
|---|---|
| libstdc++-v3 | 16N + 8B(带hash缓存) |
| libc++ | 16N + 8B |
| Visual Studio | 16N + 16B |
| Boost.Unordered | 8N + 8.5B |
N = 元素数量,B = 桶数量
架构创新:
Boost.Unordered采用桶组——32/64位占用掩码配合链接的组指针。这个设计实现常数时间迭代,同时保持教科书闭地址布局,每个桶只用4位开销。
内存省一半,性能还更好。
核心架构:
boost::unordered_flat_map用开放寻址配合基于组的组织,而不是单独桶映射。容器把桶数组分成15个桶的组,配套元数据数组存储减少的hash值和状态信息。
元数据组织:
Hash映射:
组通过hash_value / 2^(W-n)选择而不是单独桶映射,W表示架构宽度(64或32位)。
性能优化:
SIMD加速:利用SSE2/Neon同时比较多个桶的减少hash值。”这个技术实际上在常数时间检查适度数量的桶”,实现快速过滤不匹配位置。
Hash后混合:因为容器接受用户提供的质量不定的hash函数,它应用自动位混合。64位架构用xmx函数;32位用Hash Function Prospector生成的混合器。
反漂移机制:重新hash不仅在负载因子阈值触发,在删除带溢出位设置的元素时也触发,防止重复插入/删除循环的性能退化。
与absl::flat_hash_map对比基准:
开放寻址玩出花了,hash表也能这么卷。
示例1:基础mut限定符:
template<typename T>
struct mut: T
{
using T::T;
};
template<typename T>
T& as_const(T& x) { return x;}
template<typename T>
T& as_const(mut<T>& x) { return x;}
struct X
{
void foo() {}
void bar(this mut<X>&) {}
};
int main()
{
mut<X> x;
x.foo();
x.bar();
auto& y = as_const(x);
y.foo();
y.bar(); // 错误:不能从'X'转换到'mut<X> &'
X& z = x;
z.foo();
z.bar(); // 错误:不能从'X'转换到'mut<X> &'
}
示例2:通用多限定符系统:
template<typename T,typename... Qualifiers>
struct access: T
{
using qualifier_list=boost::mp11::mp_list<Qualifiers...>;
using T::T;
};
template<typename T, typename... Qualifiers>
concept qualified =
(boost::mp11::mp_contains<
typename std::remove_cvref_t<T>::qualifier_list,
Qualifiers>::value && ...);
struct mut;
struct synchronized;
template<typename T>
concept is_mut = qualified<T, mut>;
template<typename T>
concept is_synchronized = qualified<T, synchronized>;
struct X
{
void foo() {}
template<is_mut Self>
void bar(this Self&&) {}
template<is_synchronized Self>
void baz(this Self&&) {}
template<typename Self>
void qux(this Self&&)
requires qualified<Self, mut, synchronized> {}
};
int main()
{
access<X, mut> x;
x.foo();
x.bar();
x.baz(); // 错误
access<X, mut, synchronized> z;
z.bar();
z.baz();
z.qux();
}
这些示例演示用C++23显式对象参数实现”语法限定符子类型化”,但注意语义强制(如自动互斥锁)仍未实现。
用继承模拟限定符,脑洞真大。
简单逐字符方式:
void insert_line_feed(const char *buffer, size_t length,
int K, char *output) {
if (K == 0) {
memcpy(output, buffer, length);
return;
}
size_t input_pos = 0;
size_t next_line_feed = K;
while (input_pos < length) {
output[0] = buffer[input_pos];
output++;
input_pos++;
next_line_feed--;
if (next_line_feed == 0) {
output[0] = '\n';
output++;
next_line_feed = K;
}
}
}
优化memcpy版本:
void insert_line_feed_memcpy(const char *buffer, size_t length, int K,
char *output) {
if (K == 0) {
memcpy(output, buffer, length);
return;
}
size_t input_pos = 0;
while (input_pos + K < length) {
std::memcpy(output, buffer + input_pos, K);
output += K;
input_pos += K;
output[0] = '\n';
output++;
}
std::memcpy(output, buffer + input_pos, length - input_pos);
}
性能基准测试:
Intel Ice Lake上GCC 12测试(GB/s和每字节指令数):
| 方法 | 吞吐量 | 指令数/字节 |
|---|---|---|
| 逐字符 | 1.0 GB/s | 8.0 |
| memcpy | 11 GB/s | 0.46 |
| AVX2 | 16 GB/s | 0.52 |
手写AVX2实现比memcpy方式快约45%,尽管每字节用的指令稍多,提供更优的内存写入效率。
AVX2版本用向量指令处理32字节寄存器,通过shuffle mask和blend操作嵌入换行符插入。
memcpy快10倍,SIMD再快50%。
基础std::apply用法:
#include <iostream>
#include <tuple>
int sum(int a, int b, int c) {
return a + b + c;
}
void print(std::string_view a, std::string_view b) {
std::cout << "(" << a << ", " << b << ")\n";
}
int main() {
std::tuple numbers {1, 2, 3};
std::cout << std::apply(sum, numbers) << '\n';
std::tuple strs {"Hello", "World"};
std::apply(print, strs);
}
基于Lambda的tuple打印:
template <typename TupleT>
void printTupleApply(const TupleT& tp) {
std::cout << "(";
std::apply([](const auto& first, const auto&... restArgs) {
auto printElem = [](const auto& x) {
std::cout << ", " << x;
};
std::cout << first;
(printElem(restArgs), ...);
}, tp);
std::cout << ")";
}
通用for_each实现:
template <typename TupleT, typename Fn>
void for_each_tuple2(TupleT&& tp, Fn&& fn) {
std::apply([&fn]<typename ...T>(T&& ...args) {
(fn(std::forward<T>(args)), ...);
}, std::forward<TupleT>(tp));
}
变换操作:
template <typename TupleT, typename Fn>
[[nodiscard]] auto transform_tuple(TupleT&& tp, Fn&& fn) {
return std::apply([&fn]<typename ...T>(T&& ...args) {
return std::make_tuple(fn(std::forward<T>(args))...);
}, std::forward<TupleT>(tp));
}
apply配合fold expression,tuple遍历很方便。
问题代码:
Microsoft::WRL::Callback<ABI::ITypedEventHandler<
ABI::InputPane*,
ABI::InputPaneVisibilityEventArgs*>>(
&MyClass::OnInputPaneShowing).Get());
问题:&MyClass::OnInputPaneShowing创建一个指向成员函数的指针,WRL模板试图用它作为基类——C++里无效操作。
错误信息:
“is not a legal base class”
解决方案1:用Lambda:
m_showingToken = inputPane->put_Showing(
Microsoft::WRL::Callback<ABI::ITypedEventHandler<
ABI::InputPane*,
ABI::InputPaneVisibilityEventArgs*>>(
[this](auto&&... args) {
return OnInputPaneShowing(args...);
}).Get());
解决方案2:成员函数重载:
m_showingToken = inputPane->put_Showing(
Microsoft::WRL::Callback<ABI::ITypedEventHandler<
ABI::InputPane*,
ABI::InputPaneVisibilityEventArgs*>>(
this, &MyClass::OnInputPaneShowing).Get());
修正后用lambda(是个有operator()的类类型)或WRL内置的成员函数回调支持。
成员函数指针不能当基类,得包一层。
问题示例1:
void f(int, int);
void f(char*, char*);
void test(std::tuple<int, int> t)
{
std::apply(f, t); // 错误
}
编译器不能推导用哪个重载,因为缺少std::apply如何调用函数的信息。
问题示例2:
void f(int, int);
void f(char*, int);
auto test(int v)
{
return std::bind(f, std::placeholders::_1, v);
}
绑定点编译器不能确定应用哪个重载,因为调用类型未知。
变通方案1(通用):
void test(std::tuple<int, int> t)
{
std::apply([](auto&&... args) {
f(std::forward<decltype(args)>(args)...);
}, t);
}
lambda的模板实例化提供足够类型信息做正确的重载选择。
变通方案2(具体类型):
void test(std::tuple<int, int> t)
{
std::apply([](int a, int b) {
f(a, b);
}, t);
}
显式指定参数类型完全绕过重载解析问题。
apply不解析重载,包个lambda。
问题代码:
winrt::IAsyncAction Widget::InitializeNodesAsync()
{
auto lifetime = get_strong();
std::optional<winrt::IVectorView<int32_t>> numbers;
co_await winrt::resume_background();
CallWithRetry([&] {
numbers = GetMagicNumbers();
});
if (numbers == nullptr) // ← 错误检查
{
co_return;
}
co_await winrt::resume_foreground(m_uithread);
std::vector<winrt::Node> nodes;
nodes.reserve((*numbers).Size()); // ← 这里崩溃
}
问题:
“比较std::optional与nullptr检查optional是否包含等于nullptr的值,而不是是否为空。”
解决方案:
winrt::IAsyncAction Widget::InitializeNodesAsync()
{
auto lifetime = get_strong();
winrt::IVectorView<int32_t> numbers; // 去掉std::optional
co_await winrt::resume_background();
CallWithRetry([&] {
numbers = GetMagicNumbers();
});
if (numbers == nullptr) // ← 现在正确
{
co_return;
}
co_await winrt::resume_foreground(m_uithread);
std::vector<winrt::Node> nodes;
nodes.reserve(numbers.Size()); // 去掉解引用操作符
}
optional比nullptr不是检查空,是检查值。别搞混了。
核心概念:
现代64位指针浪费大量地址空间。x64系统48位虚拟寻址上,高16位未用。另外,malloc对齐分配(通常16字节边界)让低4位为零。总共约20位可用,直接在指针值内存储元数据。
基础实现结构:
template <typename T>
struct tagged_ptr {
T *Ptr;
};
关键操作:
打包数据:
元数据左移48位,mask指针顶部位,然后用位或组合:
static constexpr u64 CanonicalAddressSize = 48;
static constexpr u64 PtrMask = 0x0000FFFFFFFFFFFF;
void PackData(u16 Data) {
u64 PackedData = (u64)Data << CanonicalAddressSize;
u64 MaskedPtr = (u64)this->Ptr & PtrMask;
u64 Result = PackedData | MaskedPtr;
this->Ptr = (T *)Result;
}
提取数据:
指针右移48位恢复元数据。
检索有效地址:
清除打包位,必要时从第47位应用x64符号扩展。
实际应用:
20位白送,不用白不用。
从Boost 1.90开始,Boost.Bloom提供批量操作,”总的来说,可以大幅加速插入和查找”。
核心优化原理:
将hash位置计算与实际内存访问分离,让CPU预取隐藏延迟。
常规插入(k=1):
void insert(const value_type& x) {
auto h = hash(x);
auto p = position(h);
set(position, 1);
}
批量插入:
void insert(const std::array<value_type, N>& x) {
std::size_t positions[N];
for(std::size_t i = 0; i < N; ++i) {
auto h = hash(x[i]);
positions[i] = position(h);
prefetch(positions[i]);
}
for(std::size_t i = 0; i < N; ++i) {
set(positions[i], 1);
}
}
优化的批量查找(k>1):
用bitmask迭代减少:
std::uint64_t results = 0;
for(int j = 0; j < k; ++i) {
auto mask = results;
if(!mask) break;
do {
auto i = std::countr_zero(mask);
auto b = check(positions[i]);
results &= ~(std::uint64_t(!b) << i);
mask &= mask - 1;
} while(mask);
}
用std::countr_zero()常数时间跳过已终止列,减少不必要迭代从nk到大约n个额外分支。
性能指标:
boost::bloom::filter<int, K>(1000万元素,GCC 64位):
预取是关键,能提速2倍多。
用C++ std::map配合不相交整数区间作为key。
基础区间结构:
struct interval {
int min, max;
};
std::map<interval, std::string> m;
初始方法(有缺陷):
bool operator<(const interval& x, const interval& y) {
return x.max < y.min;
}
插入重叠区间会导致未定义行为,因为”区间顺序不是严格弱序”。
数学问题:
比较两个区间时,三种情况:
违反关联容器依赖的严格弱序要求。
可行解决方案:
提供异常抛出的比较操作符,确保只插入链式有序的元素:
struct interval_overlap: std::runtime_error {
interval_overlap(): std::runtime_error("interval overlap"){}
};
bool operator<(const interval& x, const interval& y) {
if(x.min == y.min) {
if(x.max != y.max) throw interval_overlap();
return false;
}
// ... 额外比较配合重叠检测
}
额外:异构查找:
透明比较器支持在区间内搜索整数:
struct less_interval {
using is_transparent = void;
// 区间对区间和整数对区间比较的重载操作符
};
区间当key,得保证不重叠。
标准库方式:
用std::set配合lower_bound和upper_bound:
std::set<int> x = ...;
auto first = x.lower_bound(a);
auto last = x.upper_bound(b);
while(first != last) std::cout << *first++ << " ";
不同区间边界需要不同组合:
// [a,b)
auto first = x.lower_bound(a);
auto last = x.lower_bound(b);
// (a,b]
auto first = x.upper_bound(a);
auto last = x.upper_bound(b);
Boost.MultiIndex替代方案:
提供更直观的range()操作,用谓词:
boost::multi_index_container<int> x = ...;
using namespace boost::lambda2;
// [a,b]
auto [first, last] = x.range(_1 >= a, _1 <= b);
// [a,b)
auto [first, last] = x.range(_1 >= a, _1 < b);
关键优势:
当端点反转(a > b)时,range“优雅处理情况”,返回空range而不是未定义行为——更好的API设计,”减少编程错误”。
Boost.MultiIndex的range比标准库的lower_bound/upper_bound组合好用。
核心架构:
开放寻址配合15个slot的组,每组配16字节元数据字,用于”基于SIMD的减少hash匹配和插入溢出控制”。
同步层级:
查找过程:
插入策略:
用事务性乐观插入:
API设计哲学:
容器故意省略迭代器避免死锁风险:”如果非阻塞,它们不安全,如果阻塞会增加竞争…很容易导致死锁”
访问API替代迭代:
visit()、cvisit()用于元素查找/访问visit_all()、cvisit_all()用于遍历emplace_or_visit()、insert_or_visit()用于组合操作std::execution::par容器维护兼容性:它”在所有boost::unordered_flat_map适用的实际场景都必须是有效实例化”,对key类型无特殊限制。
并发flat_map不给迭代器,用visit代替。乐观插入冲突率百万分之几,够低。
单次访问示例:
boost::concurrent_flat_map<int, int> m;
...
// 找key为k的元素并递增关联值
m.visit(k, [](auto& x) {
++x.second;
});
基于循环方式(批量访问之前):
std::array<int, N> keys;
...
for(const auto& key: keys) {
m.visit(key, [](auto& x) { ++x.second; });
}
批量访问API:
m.visit(keys.begin(), keys.end(), [](auto& x) { ++x.second; });
更简洁的语法替代上面的循环模式。批量版本通过内部流水线更高效处理多个key,预取连续操作的内存,消除cache miss停顿。
批量访问用流水线,cache miss少很多。
Raymond Chen解释Windows继续优化临界区的三个主要原因:
性能问题:几十年后bug可能罕见,但性能问题持续存在。Chen指出”小的性能问题累积成大问题”,因为临界区用得太多了。
非分页池压力:优化临界区减少内存占用成本。Chen解释,”即使非分页池的小成本乘以大量临界区,也会导致非分页池压力过大。”
优先级反转缓解:最近变化解决优先级反转问题,Windows 11 24H2”把更多工作移到用户模式,避免之前需要内核模式转换的一些情况。”
Chen总结,尽管是”老狗”,临界区继续演化处理比三十年前”更大、更快、更并发”的现代计算需求。
临界区用了几十年,还在优化。性能问题永远存在。
现象:
博客探索C++的注入类名(injected-class-name)机制如何支持写出语法正确的句子作为有效代码。著名示例:
struct buffalo {
buffalo();
};
buffalo::buffalo::buffalo::buffalo::buffalo::buffalo::buffalo::buffalo() {
// ...
}
这能编译成功,镜像语言学句子”Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo.”
关键机制:注入类名
在类作用域内,”当前类的类名或当前类模板的模板名被视为好像是公共成员名”。
这意味着buffalo::buffalo实际上无限引用自己。作者演示这个模式无论链接多少次命名空间限定符都产生相同的AST。
实际应用:
注入类名支持几种构造:
struct buffalo::buffalo::buffalo b{};第二个例子:
文章用另一个有效句子实现”James while John had had had had had…“:
namespace james_while_john {
struct had {
void a_better_effect_on_the_teacher();
};
};
void james_while_john::had::had::had::had::had::had::had::had::had::had::had::a_better_effect_on_the_teacher() {
}
这演示注入类名机制如何支持编写镜像语法复杂英文句子的代码。重复的::had::限定符能work,因为类名在自己作用域内作为成员可用,支持这种不寻常但有效的语法模式。
绕口令我靠