返回首页
最新
我喜欢观察旧平台或软件的用户界面。那种像素化的外观对我来说比现代的用户界面更具吸引力。<p>对我来说,我最喜欢的一些界面是后期PalmPilot的用户界面。尽管只有160×160像素的显示效果,它们的表现却出乎意料地好。虽然今天看起来可能不算最佳,但它们非常实用。在某种程度上,我实际上更喜欢旧的用户界面设计。现代的用户界面组件感觉更复杂,在我看来,这导致了更多的bug。<p>大家最喜欢的用户界面是什么,无论是旧的还是现代的?
我一直在构建一个跨平台的 JSONL 查看器应用程序,能够处理多个 GB 的文件。在我的开发机器 macOS 上运行得非常完美,但在 Windows 上却在 2,650 KB 时总是崩溃。以下是我的调试过程以及那个微小的修复,它改变了一切。
<p>问题</p>
- macOS:轻松处理超过 5GB 的文件
- Windows:每次在 2,650 KB 时崩溃
- 相同的代码库,从 Mac Silicon 使用 MinGW 交叉编译到 Windows
<p>调查过程</p>
添加了详细的日志记录以跟踪执行情况。崩溃发生在成功解析约 6,000 行后进行字符串驻留时。不是在解析期间,也不是在文件 I/O 期间,而是在合并阶段。
<p>根本原因</p>
我的 StringPool 类使用了 `std::unordered_map<std::string_view, uint32_t>` 来去重字符串。`string_views` 指向一个 `std::vector<std::string>`。
当向量增长并重新分配时,所有的 `string_view` 键变成了悬空指针。哈希表中充满了无效引用。
为什么在 macOS 上可以正常工作?不同的内存分配器行为、不同的默认栈大小(8MB 对比 1MB)、不同的重新分配模式。
<p>修复方案</p>
修复前(崩溃):
```cpp
uint32_t intern(std::string_view str) {
auto it = indices_.find(str);
if (it != indices_.end()) return it->second;
uint32_t idx = strings_.size();
strings_.push_back(std::string(str));
indices_[std::string_view(strings_.back())] = idx; // 危险!
return idx;
}
```
修复后:
```cpp
uint32_t intern(const std::string& str) {
auto it = indices_.find(std::string_view(str));
if (it != indices_.end()) return it->second;
// 如果即将重新分配,提前重建
if (strings_.size() >= strings_.capacity()) {
strings_.reserve(strings_.capacity() * 2);
rebuildIndices(); // 修复所有 string_views!
}
uint32_t idx = strings_.size();
strings_.push_back(str);
indices_[std::string_view(strings_.back())] = idx;
return idx;
}
void rebuildIndices() {
indices_.clear();
for (size_t i = 0; i < strings_.size(); i++) {
indices_[std::string_view(strings_[i])] = i;
}
}
```
<p>结果</p>
- 100 万行:在 Windows 上耗时 6 秒
- 多个 GB 的文件:没有崩溃
- 吞吐量约为 166,000 行/秒
- 跨平台稳定性
<p>经验教训</p>
1. `std::string_view` 功能强大但危险 - 它是一个非拥有引用。当底层存储移动时,你持有的是垃圾。
2. 跨平台测试至关重要 - 由于不同的分配器行为和较大的默认栈大小,macOS 上的这个 bug 是不可见的。
3. 结构化日志优于调试器进行交叉编译 - 我是从 Mac 交叉编译到 Windows。将带时间戳的日志记录到文件中使崩溃点立即显而易见。
4. 小改动,大影响 - 一个函数,约 15 行代码,将“在 2MB 时崩溃”变成了“处理超过 5GB 的文件”。
5. 性能保持优秀 - 重建仅在向量重新分配(指数增长)期间发生,因此摊销成本可以忽略不计。
<p>技术栈</p>
- 使用 simdjson (v4.2.2) 进行解析
- 多线程解析(在我的测试机器上使用 20 个线程)
- 列式存储以提高内存效率
- C++17,使用 MinGW-w64 进行交叉编译
这让我深刻认识到,最关键的 bug 往往是最简单的,隐藏在平台差异的表面之下。
欢迎讨论实现细节、simdjson 的使用或跨平台 C++ 调试技术!