| layout | post |
|---|---|
| title | 第193期 |
公众号
点击「查看原文」跳转到 GitHub 上对应文件,链接就可以点击了
qq群 753792291 答疑在这里
欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言
懒狗忘了
标准委员会动态/ide/编译器信息放在这里
编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期
现代GPU已经够成熟了,不需要那些复杂的图形API了。DirectX 12/Vulkan/Metal这些API是13年前为异构硬件设计的,现在GPU都统一了,还搞那么复杂干嘛?
最大的问题是PSO(管线状态对象)排列爆炸,搞出100GB+的缓存
他的想法是简化成类似CUDA的风格:
// 直接用指针分配内存
uint32* numbers = gpuMalloc(1024 * sizeof(uint32));
for (int i = 0; i < 1024; i++) numbers[i] = random();
gpuFree(numbers);
// 根参数直接传指针
struct alignas(16) Data {
float16x4 color;
const uint8* lut;
const uint32* input;
uint32* output;
};
void main(uint32x3 threadId : SV_ThreadID, const Data* data) {
uint32 value = data->input[threadId.x];
data->output[threadId.x] = value;
}
// 图形管线设置简化成这样
GpuRasterDesc rasterDesc = {
.depthFormat = FORMAT_D32_FLOAT,
.colorTargets = {{.format = FORMAT_RG11B10_FLOAT}},
};
GpuPipeline pipeline = gpuCreateGraphicsPipeline(vertexIR, pixelIR, rasterDesc);整个原型API只要150行代码,Vulkan可是20,000+行啊。能简化是好事,不过这得看硬件厂商买不买账了
Windows剪贴板对UTF-8的支持就是个坑。问题在于:
- Windows压根没有UTF-8的键盘布局或区域设置
- 转换UTF-16和8位编码时依赖CF_LOCALE,但默认值来自键盘布局语言,跟UTF-8没关系
- 现在per-process的代码页设置更乱了,不同进程对"ANSI"的理解都不一样
结论很简单:UTF-8程序就直接用CF_UNICODETEXT,别折腾其他格式了
不懂windows,就不多嘴了
这是个嵌入式固件开发系列,讲怎么在小型关键系统上用现代C++。Part 2讲为什么选C++20,Part 3讲具体的硬性规则
利用c++20 的新api来约束
嵌入式的约束很明确:
- WCET(最坏情况执行时间)要可预测
- 内存预算是死的,Flash和SRAM有多少是多少
- 固件要维护好多年
- 嵌入式编译器更新慢,用太新的标准编译器不支持
C++20有这些:
- Concepts做编译期检查
- std::span传缓冲区
- std::array管理固定容量
- std::chrono搞时间
- [[nodiscard]]强制检查返回值
1. 禁用异常和动态分配
热路径里绝对不能分配内存,要用固定容量容器:
class EventQueue final {
public:
static constexpr std::size_t kMaxEventsPerTick = 16U;
[[nodiscard]] bool try_push(std::uint16_t event) noexcept {
if(this->count_ >= kMaxEventsPerTick) {
++this->overflow_count_; // 溢出了就计数,别崩
return false;
}
this->events_[this->count_] = event;
++this->count_;
return true;
}
void clear() noexcept { this->count_ = 0U; }
[[nodiscard]] std::size_t size() const noexcept { return this->count_; }
private:
std::array<std::uint16_t, kMaxEventsPerTick> events_{};
std::size_t count_{0U};
std::uint32_t overflow_count_{0U}; // 记录溢出次数,调试用
};2. 热路径禁用虚函数
用Concepts做编译期多态:
template <typename T>
concept HardwarePlatform = requires(T p) {
{ p.initialize() } noexcept -> std::same_as<bool>;
{ p.read_inputs() } noexcept;
{ p.write_outputs() } noexcept;
};
template <HardwarePlatform P>
void run_tick(P& platform) noexcept {
platform.read_inputs();
platform.write_outputs();
}编译期就能确定调用哪个函数,不用虚函数表那一套
3. 用std::span传缓冲区
[[nodiscard]] bool build_status_line(std::span<char> out) noexcept {
if(out.size() < 8U) { return false; }
out[0] = 'O';
out[1] = 'K';
return true;
}Raymond Chen发现他们的task_sequencer会栈溢出。问题在于一堆任务同步完成的时候,协程恢复会递归调用,栈越堆越深
解决办法是强制切换线程,打断递归:
struct task_sequencer
{
task_sequencer(winrt::DispatcherQueue const& queue = nullptr)
: m_queue(queue) {}
template<typename Maker>
auto QueueTaskAsync(Maker&& maker) ->decltype(maker())
{
auto task = [&]() -> Async
{
completer completer{ current };
auto local_maker = std::forward<Maker>(maker);
auto local_queue = m_queue;
co_await suspend;
if (m_queue == nullptr) {
co_await winrt::resume_background(); // 切到后台线程
} else {
co_await winrt::resume_foreground(local_queue); // 或者切到指定队列
}
co_return co_await local_maker();
}();
// ...
}
};线程切换会强制展开栈,就不会溢出了。
Daniel Lemire测试了几种解析IPv4地址的方法,不用SIMD。结论是手动展开循环最快:
性能对比(Apple M4):
- 手动展开:114指令,3.3纳秒
- 手动循环:185指令,6.2纳秒
- std::from_chars:381指令,14纳秒
std::from_chars慢了4倍多,我操?
手动展开的代码长这样:
std::expected<uint32_t, parse_error> parse_manual_unrolled(const char *p, const char *pend) {
uint32_t ip = 0;
int octets = 0;
while (p < pend && octets < 4) {
uint32_t val = 0;
if (p < pend && *p >= '0' && *p <= '9') {
val = (*p++ - '0');
if (p < pend && *p >= '0' && *p <= '9') {
if (val == 0) { // 01.02这种不合法
return std::unexpected(invalid_format);
}
val = val * 10 + (*p++ - '0');
if (p < pend && *p >= '0' && *p <= '9') {
val = val * 10 + (*p++ - '0');
if (val > 255) { // 超过255不行
return std::unexpected(invalid_format);
}
}
}
} else {
return std::unexpected(parse_error::invalid_format);
}
ip = (ip << 8) | val;
octets++;
if (octets < 4) {
if (p == pend || *p != '.') {
return std::unexpected(invalid_format);
}
p++;
}
}
if (octets == 4 && p == pend) {
return ip;
} else {
return std::unexpected(invalid_format);
}
}就是把循环展开,少点分支判断
new char[4096]实际分配多少内存?答案是会多分配一点
Linux:请求4096字节,实际给4104字节,多8字节可以用。开销0.4%,还行
macOS:这个就夸张了。请求3585字节,给你4096字节,浪费14%!macOS喜欢按512字节对齐
可以用 malloc_usable_size查实际能用多少。这种过度分配对小对象影响大,大内存反而无所谓
Herb Sutter讲为什么C++和Rust还在快速增长
核心观点:软件消耗算力的速度比硬件进步快,性能需求永远满足不了
数据很有意思:
- 2022-2025开发者从3100万涨到4700万,增长50%
- C++和Rust增长最快
- 2025年最大瓶颈是电力供应,不是芯片
关于安全性,他的观点很实在:
- MITRE 2025报告里,10大危险问题只有3个跟语言安全有关
- 79%网络入侵是恶意软件,不是代码漏洞
- C++漏洞率比C低多了
C++26要加的新东西:
- 未初始化变量不再UB
- 标准库加边界检查(强化模式)
- 合约支持
性能需求一直在涨,C++不会过时
用 std::basic_string<uint8_t>处理二进制数据?别这么干了
问题在于 std::basic_string依赖 std::char_traits<T>,标准只保证 char/wchar_t这些类型有。uint8_t以前是"意外"能用,LLVM 19.1.0直接把基础模板删了
解决办法:
- 直接用
std::vector<uint8_t>,不要折腾 - 或者自己特化
std::char_traits<uint8_t>
vector够了
手写vector,教学向的文章。关键是要分离内存分配和对象构造,还要处理异常安全
template <typename T>
class vector {
private:
T* m_data;
std::size_t m_size; // 实际元素数量
std::size_t m_capacity; // 容量
};reserve要这么写才异常安全:
void reserve(size_type new_capacity){
if(new_capacity <= capacity()) return;
auto ptr = allocate_helper(new_capacity); // 先分配新内存
try {
copy_old_storage_to_new(m_data, m_size, ptr); // 拷贝可能抛异常
} catch(std::exception& ex){
deallocate_helper(ptr); // 失败了释放新内存
throw; // 继续抛,对象状态不变
}
std::destroy(m_data, m_data + m_size); // 成功了才销毁旧数据
deallocate_helper(m_data);
m_data = ptr;
m_capacity = new_capacity;
}先做可能失败的操作,成功了再修改状态。这是异常安全的基本套路
文章写得挺详细,不过说实话,谁手写啊。面试可能会考?
C++20加了5种新时钟,因为"世界运行在多个时间尺度上"
- utc_clock:有闰秒的UTC时间
- tai_clock:国际原子时间,1958年开始,不含闰秒
- gps_clock:GPS时间,1980年开始
- file_clock:文件系统时钟,跟
std::filesystem配套 - local_t:本地时间,但不指定时区
local_t要跟时区配合用:
auto local = std::chrono::local_time<std::chrono::minutes>{ ... };
auto tz = std::chrono::locate_zone("Europe/Berlin");
auto sys_time = tz->to_sys(local); // 转成系统时间Raymond Chen写了一系列文章,讲怎么只用前向迭代器交换内存块。为什么要研究这个?因为 std::rotate只需要前向迭代器,但常见的实现方法需要双向迭代器
- How can you swap two adjacent blocks of memory using only forward iterators?
- How can you swap two non-adjacent blocks of memory using only forward iterators
- Swapping two blocks of memory that reside inside a larger block, in constant memory, refinement
设三个指针 first、mid、last,要把块A和块B交换位置。思路是逐个交换元素,直到较小的块移动完,然后递归处理剩余部分
template<typename ForwardIt>
void rotate_adjacent(ForwardIt first, ForwardIt mid, ForwardIt last) {
if (first == mid || mid == last) return;
auto p = first;
auto q = mid;
while (true) {
std::iter_swap(p++, q++); // 交换元素
if (p == mid) {
// 块A用完了,块B还有剩余
if (q == last) return; // 都用完了
mid = q; // 递归处理剩余的
} else if (q == last) {
// 块B用完了,块A还有剩余
// 递归处理剩余的
return rotate_adjacent(p, mid, last);
}
}
}复杂度O(n)次交换,O(1)空间,挺优雅的
更复杂的场景:内存排列是 A1, A2, B1, B2, C1, C2, D1, D2, D3, E1,要交换B和D
template<typename ForwardIt>
void swap_nonadjacent(ForwardIt b_start, ForwardIt b_end,
ForwardIt d_start, ForwardIt d_end) {
auto p = b_start;
auto q = d_start;
// 同时遍历B和D,交换元素
while (p != b_end && q != d_end) {
std::iter_swap(p++, q++);
}
// 哪个块先用完,就旋转剩余部分
if (p == b_end && q != d_end) {
// B用完了,旋转剩余的D和C
rotate_adjacent(b_start, q, d_end);
} else if (q == d_end && p != b_end) {
// D用完了,旋转剩余的B和C
rotate_adjacent(p, b_end, d_start);
}
}总交换次数还是n次(最优)
评论区有人(Neil Rashbrook)提了个优化:原来的方法要三次旋转(2n次交换),可以优化成两次旋转:
- 旋转BCD把D移到前面
- 旋转BC把C移到前面
// 第一步:旋转BCD,把D移到前面
// A1 A2 | B1 B2 C1 C2 D1 D2 D3 | E1
// 变成:A1 A2 | D1 D2 D3 B1 B2 C1 C2 | E1
rotate_adjacent(b_start, d_start, d_end);
// 第二步:旋转BC,把C移到前面
// A1 A2 D1 D2 D3 | B1 B2 C1 C2 | E1
// 变成:A1 A2 D1 D2 D3 C1 C2 B1 B2 E1
rotate_adjacent(after_d, b_new_pos, c_end);交换次数变成2n − max(|B|,|D|),省了点
这系列文章挺有意思的
讲怎么找Windows的插入符(caret,就是那个闪烁的光标)位置
- How can I find out where the Windows caret is?
- Using Active Accessibility to find out where the Windows caret is
GetCaretPos只能获取当前线程的插入符,要获取全局的得用 GetGUIThreadInfo:
GUITHREADINFO info = { sizeof(info) };
if (GetGUIThreadInfo(0, &info)) {
if (info.flags & GUI_CARETBLINKING) {
MapWindowPoints(info.hwndCaret, nullptr, (POINT*)&info.rcCaret, 2);
SetCursorPos(info.rcCaret.right - 1, info.rcCaret.bottom - 1);
}
}问题是很多现代应用(VS、Chrome、Word)用的是自定义插入符,GUI_CARETBLINKING标志不会设置。这时候要用Active Accessibility接口:
GUITHREADINFO info = { sizeof(GUITHREADINFO) };
if (GetGUIThreadInfo(0, &info)) {
if (info.hwndFocus != nullptr) {
Microsoft::WRL::ComPtr<IAccessible> acc;
if (SUCCEEDED(AccessibleObjectFromWindow(info.hwndFocus, OBJID_CARET,
IID_PPV_ARGS(&acc))) && acc) {
long x, y, cx, cy;
VARIANT vt{};
vt.vt = VT_I4;
vt.lVal = CHILDID_SELF;
if (acc->accLocation(&x, &y, &cx, &cy, vt) == S_OK) {
SetCursorPos(x + cx - 1, y + cy - 1);
return;
}
}
}
}这套能在大多数应用里工作,但Windows Terminal和计算器不支持
