公众号

点击「查看原文」跳转到 GitHub 上对应文件,链接就可以点击了
qq群 753792291 答疑在这里
欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言
本期文章没人赞助
标准委员会动态/ide/编译器信息放在这里
编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期
Arthur O’Dwyer记录了C++26 P2447(给span加上从initializer_list构造的能力)在Chromium里的实际收益
这个特性允许span<const T>直接从{a, b, c}这样的初始化列表构造,不需要先建个临时vector或array。Chromium在2024年底给base::span加了同样的构造函数,顺带清理了一大堆调用点
典型的before/after:
// Before:先建vector,再传给CertCache
std::vector<scoped_refptr<const Cert>> certs(
{kcer_cert_0, kcer_cert_1, kcer_cert_2, kcer_cert_3,
kcer_cert_3, kcer_cert_2, kcer_cert_1, kcer_cert_0,
kcer_cert_0, kcer_cert_2, kcer_cert_3, kcer_cert_1});
CertCache cache(certs);
// After:直接传
CertCache cache({kcer_cert_0, kcer_cert_1, kcer_cert_2, kcer_cert_3,
kcer_cert_3, kcer_cert_2, kcer_cert_1, kcer_cert_0,
kcer_cert_0, kcer_cert_2, kcer_cert_3, kcer_cert_1});
// Before:临时std::array
ASSERT_TRUE(ConfigureAppContainerSandbox(
std::array<const base::FilePath*, 2>{&pathA, &pathB}));
// After:直接传
ASSERT_TRUE(ConfigureAppContainerSandbox({&pathA, &pathB}));
文章还总结了几个”失败案例”——主要是CTAD的缺失。比如span(box.type) == {'f','t','y','p'}过不了,原因是语法规定==后面不能跟braced-initializer-list(除co_yield和赋值运算符外)
有趣的是,这些调用点为了表达”两个元素的视图”,各自发明了三种不同的workaround(临时array、双括号、显式cast),一有了正确的语法,全都收敛到同一种写法了。语言就应该这样
省流:C++26之后span<const T>参数直接传{a, b}就行了
Lemire测了现代处理器的分支预测器到底能记住多少条分支
测试套路:把同一段随机数序列重复跑,让CPU有机会”学习”分支模式,看多少分支能被完美预测
while (howmany != 0) {
val = generate_random_value();
if (val is odd) write to buffer;
decrement howmany;
}
结果:
| 处理器 | 可完美预测的分支数 |
|---|---|
| AMD Zen 5 | 30,000 |
| Apple M4 | 10,000 |
| Intel Emerald Rapids | 5,000 |
Lemire:我又一次对Intel感到失望。AMD在这个benchmark上干得非常好
对做benchmark的人来说有个实际意义:小数据集测出来的结果可能完全不代表真实场景,因为CPU学会了你的测试数据。
bunnie(知名硬件黑客,写过《芯片设计》那本书)在设计自己的22nm SoC Baochip-1x时,造了个I/O协处理器BIO作为Raspberry Pi PIO的替代品
先研究PIO: 发现PIO虽然只有9条指令,但是典型CISC思路——每条指令带barrel shifter、wrap-around、FIFO管理、side-set……移植到FPGA后发现PIO用的逻辑单元比RISC-V CPU核还多,critical path是RISC-V的两倍
BIO的设计: 直接用PicoRV32(RV32E,16个寄存器),然后把r16-r31扩展成特殊功能寄存器:
x16-x19:8深度FIFO的头/尾访问,空时read-halt,满时write-halt
x20:snap-to-quantum,halt直到下一个时钟量子
x21-x26:GPIO读写/方向控制
x27-x30:事件寄存器,条件阻塞
x31:core ID + 时钟计数
面积对比:BIO 14597 cells vs PIO 39087 cells,BIO大约是PIO一半面积,时钟速率是PIO的4倍以上
代码用标准RV32E汇编写,甚至支持C(通过Zig的clang编译器),不需要学PIO那套私有指令集。三个核并行做DMA、SPI等外设协议
这是硬件设计文章,不是纯C++内容。不过思路很清晰,RISC vs CISC的经典权衡在I/O协处理器上的体现。感兴趣的可以点进去看看
Mathieu Ropert终于找到了coroutines在游戏里的具体用武之地
问题: 游戏里有很多跨帧的特效/行为,比如一个物体先左移一格,再右移四次,再旋转。写成state machine极其丑陋:
class TimeWarp {
enum class State { Jump, StepRight, HandsOnHips, DoAgain };
State _state = State::Jump;
int _i = 0;
Transform* _transform;
bool operator()() {
switch (_state) {
case State::Jump:
_transform->position.x -= 1.f;
_state = State::StepRight;
break;
case State::StepRight:
_transform->position.x += 0.2f;
if (++_i == 4) { _state = State::HandsOnHips; _i = 0; }
break;
// ...
}
return false;
}
};
用C++23 generator写:
std::generator<std::monostate> TimeWarp(GameObject& obj)
{
// It's just a jump to the left
obj.transform.position.x -= 1.f;
co_yield {};
// Then a step to the right
for (int i = 0; i < 4; ++i) {
obj.transform.position.x += 0.2f;
co_yield {};
}
// Let's do the time warp again!
for (int i = 0; i < 4; ++i) {
obj.transform.Rotate(0.f, 90.f * i, 0.f);
co_yield {};
}
}
co_yield {}的语义是”本帧做到这里,下帧继续”。跟Unity的yield return null一毛一样——Unity当年也是因为没有await,用yield hack了这个语义,作者亲切地称之为”The Unity Hack”
调度器不到100行:
class effects_manager {
public:
void add(std::generator<std::monostate> effect) {
_effects.push_back(std::move(effect));
_iterators.push_back(_effects.back().begin());
}
void run() {
// 清理已完成的coroutine
int first = 0;
for (; first != _effects.size()
&& _iterators[first] != _effects[first].end(); ++first);
if (first != _effects.size()) {
for (int i = first; ++i != _effects.size(); ) {
if (_iterators[i] != _effects[i].end()) {
_effects[first] = std::move(_effects[i]);
_iterators[first] = std::move(_iterators[i]);
++first;
}
}
_effects.erase(begin(_effects) + first, end(_effects));
_iterators.erase(begin(_iterators) + first, end(_iterators));
}
// 推进所有特效一帧
for (int i = 0; i < _effects.size(); ++i)
++_iterators[i];
}
private:
std::vector<std::generator<std::monostate>> _effects;
using effect_iterator =
decltype(std::declval<std::generator<std::monostate>>().begin());
std::vector<effect_iterator> _iterators;
};
用法:effects.add(TimeWarp(obj)),主循环里调effects.run(),完活
文章末尾还给出了更纯粹的版本——让coroutine yield一个Draw对象,run()收集所有Draw对象,顺手还能parallel_for并行推进
这个use case比Fibonacci生成器实在多了,不用co_await也不用搞一套executor。做游戏/动画的同学强烈推荐
SereneDB团队造了个新的I/O buffer(sdb::message::Buffer),跟folly::IOBuf、absl::Cord等比了一下
设计: 本质是链表形式的chunk队列,chunk大小指数增长(bounded by min_growth和max_growth)。关键点:这是个SPSC(单生产者单消费者),只有一个atomic变量_send_end,lock-free极简
支持三种数据状态:
class Buffer {
public:
Buffer(size_t min_growth, size_t max_growth,
size_t flush_size = std::numeric_limits<size_t>::max(),
std::function<void(SequenceView)> send_callback = {});
void WriteUncommited(std::string_view data);
void Write(std::string_view data, bool need_flush);
void Commit(bool need_flush);
[[nodiscard]] uint8_t* GetContiguousData(size_t capacity);
template<typename Op>
void WriteContiguousData(size_t capacity, Op op);
};
benchmark数据(AMD Ryzen 9 9950X + jemalloc):
| 场景 | folly appender | sdb buffer | sdb快多少 |
|---|---|---|---|
| 1亿包,中位14B | 887ms | 884ms | 0.28% |
| 100万包,中位0.3KB | 28ms | 26ms | 6.96% |
| 100万包,中位2.6KB | 366ms | 234ms | 36.09% |
| 1万包,中位82KB | 90ms | 50ms | 45.06% |
大包场景下比folly快接近一半。SPSC限制有点强,但场景合适的时候很香
代码:https://github.com/serenedb/serenedb
Sandor Dargo总结了C++26给std::span带来的四个改进
P2447R6:span从initializer_list构造(上面Arthur那篇讲过了)
void take(std::span<const int> v);
take({1, 2, 3}); // C++26前报错,C++26后OK
P2821R5:span.at()
终于有bounds-checked访问了:
std::array<int, 4> arr = {1, 2, 3, 4};
std::span<int> s{arr};
s[10]; // undefined behaviour
s.at(10); // throws std::out_of_range
string、string_view、vector、array、deque都有at(),span一直是孤儿,现在补上了
P2833R2:Freestanding里的span/expected
嵌入式场景能用了(span::at()因为会throw,不进freestanding子集)
P3029R1:更智能的mdspan CTAD
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
// C++26后:deduced as span<int, 5>,保留编译期大小信息
std::span s(p, std::integral_constant<std::size_t, 5>{});
都是实用的小修小补,感兴趣的可以点进去看看
Elmar Westphal在Forschungszentrum Jülich做了15年GPU程序员,主要是分子动力学/微磁学模拟,CUDA老手一枚,讲的是用标准C++的execution policy做GPU编程
GPU编程的演化路径:
nvc++这类编译器支持直接用std::execution::par_unseq跑到GPU上核心思路:你写的是标准C++,换个编译器flag就能跑在GPU上:
std::for_each(std::execution::par_unseq, data.begin(), data.end(),
[](auto& x) { x = compute(x); });
用nvc++ -stdpar=gpu编译,上面这段代码就会生成GPU kernel。不写CUDA、不写kernel,代码可以同时跑CPU和GPU
实际效果: 对很多workload,生成的GPU代码性能和手写CUDA相当。不是所有场景都适合(legacy代码、复杂内存访问模式有限制),但对新代码来说portability极高
这个话题隔一段时间就有人拿出来讲一次,但这次讲的人是实际用CUDA多年的人,可信度高一些。Slides: Back_to_the_Standard.pdf
Nico Eichhorn(Bosch)讲Mutation Testing,这个是比代码覆盖率更狠的测试质量评估手段
原理: 工具自动对你的代码做微小的”变异”(mutant):
// 原代码
if (x > 0) return true;
// 变异1:改运算符
if (x >= 0) return true;
// 变异2:翻转条件
if (x <= 0) return true;
// 变异3:直接返回
return true;
每个mutant跑一遍测试套件:
最终得到一个mutation score = killed / total,越高越好
为什么比coverage更有用: 100%覆盖率只说明每行代码被执行过,不说明你的断言足够强。Mutation testing直接验证测试能不能发现bug
演讲包含Bosch真实项目的case study。C++主流工具是mull(LLVM-based)
Gareth Williamson讲多语言项目里C++如何跟其他语言互操作
各语言的C++互操方案:
C++互操的特殊挑战:
最佳实践:用extern “C”包一层C接口,或者用专门的interop工具(cxx、pybind11)处理转换。Slides: https://slides.meetingcpp.com
Stockholm C++ Meetup,Kristian Ivarsson介绍他们用C++写的分布式应用服务器Casual
Casual的定位: 类似Tuxedo/WebLogic那个年代的企业中间件,但是现代化、开源、UNIX风格,用C++写的
核心特性:
面向的场景是需要事务保障的大规模分布式系统,不是互联网那套HTTP+微服务,而是金融/电信那套更严肃的场景
GitHub: https://github.com/casualcore/casual
Rare的Keith Stockdale讲《盗贼之海》把C++标准从C++14升到C++20的完整经历,干货很多
代码规模:
| 项目 | 源文件数 | 代码行数 |
|---|---|---|
| Sea of Thieves游戏项目 | 28,269 | 977,148 |
| Engine项目 | 19,419 | 449,028 |
| 其他 | 579 | 56,057 |
加起来约150万行,每个release还在持续新增代码。在这规模上升级C++标准,心理建设得做好
升级的第一道坎:MSVC /permissive-
C++20支持的关键前提是先开启MSVC的/permissive-(标准符合模式)。这个flag让MSVC严格遵循C++标准,关掉各种历史遗留的非标准扩展
但问题是:MSVC默认不开这个flag,而且他们的代码已经依赖了大量非标准行为。于是第一步就是:
开 /permissive- → 一堆编译错误 → 修 → 重复
策略:拆成小changelists,每个只改一点,easier to review,出问题也容易回滚。全部修完花了约4个月
实际踩的坑(有slides代码为证):
坑1:Two-phase name lookup。MSVC历史上不支持正确的两阶段名字查找,模板体里的dependent name在定义时不查找只在实例化时查找,而/permissive-开启后才强制执行标准行为。于是一堆老模板代码炸了
坑2:C++20 operator== rewriting。C++20会自动为operator==生成reversed和symmetric版本,用来支持a == b和b == a的统一处理。但如果代码里已经手写了对称的operator==,编译器看到a == b就有两个候选,ambiguous:
error C2666: 'bool MyStruct::operator==(MyStruct)' 2 overloads have similar conversions
note: C++20 says that these are ambiguous even though the second is a restatement of the first
要么删掉冗余的,要么显式用= default
坑3:各种非标准写法积累。十年老代码库里总有各种依赖MSVC扩展的代码,/permissive-统统揪出来
跨平台编译器加持: 他们用GCC、Clang、MSVC三套编译器一起编,互相补充,能发现只有某个编译器能抓到的问题
build时间对比(AMD Threadripper 3975W, 128GB RAM): 升级后build time没有变差——这是他们比较担心的问题,结果是all good
获得的新特性:
#if PLATFORM_PS5之类的条件编译最终结论(slides原话):
150万行游戏代码,现役游戏,没有固定deadline,一边出feature一边搞升级,花了约4个月把/permissive-打开。不是什么”升级只要改一行flag”的爽文,是实实在在的工程案例
Slides: Changing_Cpp14_To_Cpp20.pdf