公众号

点击「查看原文」跳转到 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 = ,
};
GpuPipeline pipeline = gpuCreateGraphicsPipeline(vertexIR, pixelIR, rasterDesc);
整个原型API只要150行代码,Vulkan可是20,000+行啊。能简化是好事,不过这得看硬件厂商买不买账了
Windows剪贴板对UTF-8的支持就是个坑。问题在于:
结论很简单:UTF-8程序就直接用CF_UNICODETEXT,别折腾其他格式了
不懂windows,就不多嘴了
这是个嵌入式固件开发系列,讲怎么在小型关键系统上用现代C++。Part 2讲为什么选C++20,Part 3讲具体的硬性规则
利用c++20 的新api来约束
嵌入式的约束很明确:
C++20有这些:
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):
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还在快速增长
核心观点:软件消耗算力的速度比硬件进步快,性能需求永远满足不了
数据很有意思:
关于安全性,他的观点很实在:
C++26要加的新东西:
性能需求一直在涨,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够了
<T>手写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种新时钟,因为”世界运行在多个时间尺度上”
std::filesystem配套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只需要前向迭代器,但常见的实现方法需要双向迭代器
设三个指针 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移到前面
// 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,就是那个闪烁的光标)位置
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和计算器不支持