C++ 中文周刊 2025-12-19 第192期

周刊项目地址

公众号

点击「查看原文」跳转到 GitHub 上对应文件,链接就可以点击了

qq群 753792291 答疑在这里

RSS

欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言

本期文章由 赞助老爷 赞助 在此表示感谢


资讯

标准委员会动态/ide/编译器信息放在这里

编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期

性能周刊

文章

C++结构化迭代

这篇文章讲解如何用安全的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就别手写索引。

用Lambda工厂创建Functor

文章讨论了从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];
  };
}

更快的double转string

Victor Zverovich展示了新的浮点转字符串算法zmij,显著超越现有方法,原则是”不做比做点什么更快”。

性能基准:

单个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};
}

主要改进:

  1. 减少候选数:从2-4个候选选择减少到1-3个
  2. 更少乘法:特别是在短路径
  3. 无分支操作:消除预测不良的分支
  4. 查找表:数字对输出减半整数乘法
  5. 简化不规则舍入:无分支处理边缘情况

应用:

代码仓库:https://github.com/vitaut/zmij

vpternlog实现有符号饱和运算

这篇文章探讨如何用Intel的vpternlog{d,q}指令——三操作数位逻辑运算——在x86-64处理器上实现32位和64位有符号饱和运算。

vpternlog指令:“三个操作数的位三元逻辑,能实现任何三操作数布尔函数”,通过8位查找表索引三个输入操作数的位。

饱和运算:防止溢出/下溢回绕,把结果钳位到最大值(0x7FFFFFFF)或最小值(0x80000000)。

检测逻辑:

加法,输入符号相同但结果符号不同时饱和:

减法,输入符号不同且结果符号与第一操作数不同时饱和:

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;
}

算法总结:

  1. 执行算术运算(加/减)
  2. vpternlog根据符号位检测溢出/下溢
  3. 通过vpmov{d,q}2m将检测结果转换为执行掩码
  4. 算术右移结果以扩展符号位
  5. XOR饱和常量生成适当边界

C++矩阵乘法

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

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这样的工具验证,并始终基准测试。”

归纳变量把乘法变加法,但手动优化可能因循环依赖反而更慢。信编译器。

循环不变代码外提(LICM)

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值

关键洞察:

作者指出”gcc似乎无法利用这个优化”,提交bug建议问题与结构化返回类型的公共子表达式消除限制相关。

LICM把循环里不变的代码提到外面,但不是所有编译器都聪明,GCC就没做好。

LICM失效的情况

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

关键洞察:

文章强调:”别名是C++更隐蔽的陷阱之一…你无法避免使用它们。”

类型相同可能别名,编译器不敢优化。用不同类型或__restrict能帮上忙。

调用约定

Matt Godbolt解释调用约定——管理x86 Linux上函数参数如何传递的ABI规则。演示编译器如何基于数据类型和参数数量优化参数传递。

System V ABI基础:

“前几个参数放在rdirsi“用于整数类型和指针。

结构打包:

当传递包含两个long值的结构时,编译器高效地将两者打包进寄存器rdirsi,生成与独立参数版本相同的汇编。

寄存器溢出示例(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_viewstd::optional这样类型的设计决策,帮助开发者优化参数传递并实现更好的编译器优化。

前6个参数放寄存器,超出的上栈。结构能打包进寄存器。懂调用约定能写出编译器友好的代码。

内联——终极优化

Matt Godbolt解释内联——用函数体替换函数调用——根本上重要不是为了消除调用开销,而是为了启用后续编译器优化。

内联启用什么:

示例:字符串大小写转换

文章用change_case工具函数演示。当内联到make_upperupper总是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

好处:

这种方法实现”两全其美:我们获得内联轻量检查和简单算术的性能好处,同时避免复制昂贵计算导致的代码膨胀。”

部分内联把函数拆成快路径和慢路径,快路径内联,慢路径调用。性能和代码大小都照顾到了。

C++前向声明:好与坏

基础前向声明:

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;
}

好处:

安全措施:

前向声明省编译时间,但得小心delete。用static_assert保平安。

找到VS Code内存泄漏

Bruce Dawson通过一个不寻常的观察发现泄漏:同事系统上的进程ID有七位数。因为Windows PID是4的倍数且快速复用,”四百万左右的PID意味着一百万个进程”,说明是进程句柄泄漏而不是真的进程泄漏。

根本原因:

vscode-windows-process-tree的buggy代码调用OpenProcess()但没对应的CloseHandle()。每个未关闭的句柄泄漏大约64 KB,最终消耗”大约64 GB”内存,没有上限。

调查技术:

  1. 任务管理器分析:句柄列显示哪个进程在泄漏
  2. ETW跟踪:Windows事件跟踪提供调用栈,定位确切的泄漏代码位置
  3. 模式识别:高PID值标志句柄保留问题

修复:

加一行解决:CloseHandle(hProcess);

关键洞察:

Dawson主张用RAII模式防止这类泄漏,建议自动资源限制能在测试阶段而不是生产环境抓到这些bug。

C++26: std::optional的range支持

基础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性能可能不如预期

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"; }));

延迟求值省开销,但转换链要小心。

提升std::unordered_map实现的技术水平

Boost.Unordered 1.80三大优化:

  1. 减少内存占用:”显著减少的内存占用改善cache利用率”
  2. 快速模数实现:用计算代替昂贵的表跳转,消除模数操作
  3. 更少指针间接:比libstdc++-v3和libc++少一次指针解引用

内存开销对比(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内部机制

核心架构:

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表也能这么卷。

C++23用户定义类限定符

示例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%。

C++模板:遍历std::tuple用std::apply

基础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遍历很方便。

学习读C++编译器错误:不是合法的基类

问题代码:

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内置的成员函数回调支持。

成员函数指针不能当基类,得包一层。

std::apply为什么搞不清我要用哪个重载?只有一个能work!

问题示例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。

null指针崩溃,尽管检查了null

问题代码:

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::optionalnullptr检查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不是检查空,是检查值。别搞混了。

C++指针标记:把位塞进指针的艺术

核心概念:

现代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.Bloom批量操作

从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倍多。

Maps on chains

用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,得保证不重叠。

有序容器的API人机工程学

标准库方式:

std::set配合lower_boundupper_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组合好用。

boost::concurrent_flat_map内部机制

核心架构:

开放寻址配合15个slot的组,每组配16字节元数据字,用于”基于SIMD的减少hash匹配和插入溢出控制”。

同步层级:

  1. 容器级别:读写互斥锁实现为跨cache line的spinlock数组,线程局部构造时round-robin分配
  2. 组级别:每个组有专用读写spinlock加原子插入计数器

查找过程:

插入策略:

用事务性乐观插入:

API设计哲学:

容器故意省略迭代器避免死锁风险:”如果非阻塞,它们不安全,如果阻塞会增加竞争…很容易导致死锁”

访问API替代迭代:

容器维护兼容性:它”在所有boost::unordered_flat_map适用的实际场景都必须是有效实例化”,对key类型无特殊限制。

并发flat_map不给迭代器,用visit代替。乐观插入冲突率百万分之几,够低。

boost::concurrent_flat_map批量访问

单次访问示例:

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少很多。

Windows为什么还在折腾临界区

Raymond Chen解释Windows继续优化临界区的三个主要原因:

  1. 性能问题:几十年后bug可能罕见,但性能问题持续存在。Chen指出”小的性能问题累积成大问题”,因为临界区用得太多了。

  2. 非分页池压力:优化临界区减少内存占用成本。Chen解释,”即使非分页池的小成本乘以大量临界区,也会导致非分页池压力过大。”

  3. 优先级反转缓解:最近变化解决优先级反转问题,Windows 11 24H2”把更多工作移到用户模式,避免之前需要内核模式转换的一些情况。”

Chen总结,尽管是”老狗”,临界区继续演化处理比三十年前”更大、更快、更并发”的现代计算需求。

临界区用了几十年,还在优化。性能问题永远存在。

buffalo::buffalo::buffalo…

现象:

博客探索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。

实际应用:

注入类名支持几种构造:

第二个例子:

文章用另一个有效句子实现”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,因为类名在自己作用域内作为成员可用,支持这种不寻常但有效的语法模式。

绕口令我靠

开源项目介绍


上一期

本期

下一期

看到这里或许你有建议或者疑问或者指出错误,请留言评论! 多谢! 你的评论非常重要!也可以帮忙点赞收藏转发!多谢支持! 觉得写的不错那就给点吧, 在线乞讨 微信转账