第6.10章: fast_io 视图
本章为何存在
在上一章中,你了解到 C 风格数组参数以及 指针加长度组合容易出错:
- 数组传给函数时大小会丢失
- 指针加长度的组合很容易配错
- 没有边界检查
- 所有权不清晰
- 鼓励不安全的模式
现代 C++ 用视图类型解决了这个问题:轻量的、 不拥有内存的对象,它们引用已有内存而不复制它。
C++ 标准库提供了 std::span 与
std::string_view,但在 Windows x86-64 上,
std::span 受到调用约定限制:
- Windows x86-64 ABI 会通过内存传递所有 16 字节对象
std::span是 16 字节(指针 + 大小)- 因此它无法通过寄存器传递
- 这引入了不必要的内存流量
这一 ABI 行为由本教程的作者发现,并向微软报告。
在 ABI 修复之前,std::span 在 Windows x86-64 上
不如简单的 (ptr, n) 组合高效。
wincall 提案
为了解决这些长期存在的 ABI 低效问题,作者提出了一种 名为 wincall 的新调用约定,专为 Intel APX 下的 x86-64 Windows 设计。相关文档见:
以及此处的讨论:
wincall 的动机包括:
- 当前的 Windows ABI 通过寄存器传递的参数太少
- 16 字节对象(如
std::span)被迫放到栈上 - 赫布异常(P0709)需要两个寄存器加进位标志来表示
std::error - Intel APX 把寄存器文件从 16 个扩展到 32 个
- 空对象不应占用寄存器槽位
- Windows 已经使用调用者保存寄存器,使扩展变得可行
wincall 的目标是:
- 使用更多寄存器来传递参数
- 避免惩罚小的聚合体
- 保持与 Itanium C++ ABI 的兼容性
- 在 DLL 边界上与 stdcall 和 fastcall 共存
- 支持高效地返回小型用户定义类型
本章并不依赖 wincall,但理解 ABI 限制有助于解释为什么
std::span 今天在 Windows 上效率不高。
fast_io 的定位
fast_io 库并不“修复”Windows
ABI。相反:
- fast_io 不是 ABI 稳定的
- 因此它可以挑选能避开 16 字节代价的布局
- 它提供更安全的语义(边界检查、显式构造)
- 它避免了裸指针和指针加长度组合的陷阱
在本章中,你将学到如何使用以下 fast_io 视图类型:
::fast_io::span<T>::fast_io::string_view::fast_io::cstring_view::fast_io::index_span<T, N>
这些类型比裸指针更安全,并避开了影响 std::span 的
Windows ABI 低效问题。
1. ::fast_io::span<T>
::fast_io::span<T> 是对 T 连续序列的轻量、
不拥有内存的视图。它存储:
- 一个指针
- 一个长度
与 std::span 不同,它有意在大小上避开
Windows x86-64 的 16 字节代价。由于 fast_io 不是 ABI 稳定的,它可以
选择一种在所有主流平台上都能高效通过寄存器传递的表示。
示例:传递 span
void process(::fast_io::span<int const> s)
{
for(auto e : s)
::fast_io::io::println(e);
}
示例:从 vector 创建 span
::fast_io::vector v{1,2,3,4};
process(::fast_io::span{v});
示例:从裸数组创建 span
int arr[4]{1,2,3,4};
process(::fast_io::span{arr});
2. ::fast_io::string_view
::fast_io::string_view 类似 std::string_view,
但被设计为在所有主流平台上(包括 Windows x86-64)对 ABI 友好。
它:
- 不做分配
- 不拥有内存
- 能高效地传给函数
- 可与
fast_io打印集成
示例
void greet(::fast_io::string_view name)
{
::fast_io::io::println("Hello, ", name);
}
3. ::fast_io::cstring_view
::fast_io::cstring_view 是对以空字符结尾的 C 字符串的
视图。它只存储一个指针,在需要时再计算长度。
它非常适合与 C API 或遗留代码交互。
示例
void debug(::fast_io::cstring_view s)
{
::fast_io::io::println("debug: ", s);
}
4. ::fast_io::index_span<T, N>
::fast_io::index_span<T, N> 是对恰好 N 个元素的
固定大小视图。它:
- 只存储一个指针
- 索引时执行边界检查
- 无需复制即可用于切片
示例
void print_slice(::fast_io::index_span<std::size_t, 5> idx)
{
for(std::size_t i{}, n{idx.size()}; i != n; ++i)
::fast_io::io::println(idx[i]);
}
5. 性能对比
std::span 与 ::fast_io::span 的关键区别
在于它们在不同 ABI 下的行为。
| 类型 | 大小 | 寄存器传递(SysV) | 寄存器传递(Win64) | 说明 |
|---|---|---|---|---|
T* |
8 字节 | 是 | 是 | 基线指针语义 |
(T*, std::size_t) |
16 字节 | 是 | 否 | 在 Win64 上通过内存传递 |
std::span<T> |
16 字节 | 是 | 否 | 与 (ptr, n) 相同的代价 |
::fast_io::span<T> |
8 字节 | 是 | 是 | 避开 Win64 的内存传递代价 |
::fast_io::span 有意在大小上模仿裸指针,在 ABI 层面
确保在所有主流平台上都能高效通过寄存器传递。
6. 何时使用哪种类型
-
对数组或 vector 的非拥有视图,使用
::fast_io::span<T>。 -
对 UTF-8 字符串切片,使用
::fast_io::string_view。 -
与 C API 交互时,使用
::fast_io::cstring_view。 -
对固定大小切片与索引区间,使用
::fast_io::index_span<T, N>。 - 避免 C 风格数组参数与 指针加长度组合。
- 在 Windows x86-64 上,性能关键的代码请避免 std::span。
要点
- fast_io 视图类型为裸指针提供了更安全的替代品。
- 它们避开了影响
std::span的 Windows x86-64 ABI 代价。 - 它们与
fast_io容器及打印自然地集成。 - 它们应当取代 C 风格数组参数以及指针加长度组合。