代码重构前后,用现代C++更安全、更简洁的方式来处理数组和长度问题,并且利用静态分析(SA,Static Analysis)工具来捕获潜在错误。
代码重构前(Before)
void f(_In_reads_(num) Thing* things, unsigned count) {
unsigned totalSize = 0;
for (unsigned i = 0; i <= count; ++i) // 错误:i <= count,越界了!
totalSize += things[i].GetSize();
// 这里会越界访问 things[count]
memcpy(dest, things, count); // 错误:count 是元素个数,但 memcpy 需要字节数
// 这些错误可以被静态分析工具检测出来
}
void caller() {
Thing things[10]; // 未初始化数据
f(things, 10);
}
问题点:
for
循环条件写成了i <= count
,导致访问越界。memcpy
参数传入的count
是元素数量,不是字节数,可能导致拷贝异常。things
数组未初始化,可能产生未定义行为。- 静态分析工具(SA)可以发现这些错误。
代码重构后(After)
void f(array_view<const Thing> things) {
unsigned totalSize = 0;
for (auto& thing : things)
totalSize += thing.GetSize();
std::copy(things.begin(), things.end(), dest); // 用 std::copy 更安全且表达语义清晰
}
void caller() {
Thing things[10]; // 未初始化数据
f(things); // 调用时传入整个数组
f({things, 5}); // 明确只传入前5个元素
f({things, 30}); // 错误,长度超过数组大小,静态分析会捕获
}
优势:
- 使用
array_view
(或者类似的std::span
)传入数据和长度,避免手动传递指针+长度的组合,减少越界风险。 - 用范围for循环遍历,更简洁且不易错。
std::copy
替代了memcpy
,类型安全,且语义更明确。- 调用时可以用花括号初始化,静态分析会检查长度是否合理,未初始化数据访问也会被警告。
- 代码更现代、更安全,维护性更高。
总结
- 避免手动管理指针和长度,使用范围封装(
array_view
、std::span
)可以减少越界和传参错误。 - 用范围for和标准库算法替代传统的索引循环和C风格函数,更安全且更易读。
- 静态分析工具结合现代C++特性能早期捕获潜在错误,提高代码质量。
- 代码更符合现代C++最佳实践。
#include <span>
#include <iostream>
void f(std::span<const int> data) {
for (auto v : data) std::cout << v << ' ';
std::cout << '\n';
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
f(arr); // 自动转换成 std::span
f(std::span(arr, 3)); // 传前3个元素的视图
}
cmake_minimum_required(VERSION 3.10)
project(HelloTestableWorld)
set(CMAKE_CXX_STANDARD 26)
# 添加你的源文件
set(SOURCES
main.cpp
)
# 添加可执行文件(主程序)
add_executable(test ${SOURCES})
这是在介绍 Microsoft GSL (Guidelines Support Library) 的目标,核心思想是:
- 从设计层面保障安全,避免危险的系统编程错误(例如内存错误)。
- 阻止有缺陷的代码编译通过,提早在编译时发现问题。
- 运行时检测缺陷,快速失败,避免隐藏bug导致更大问题。
- 保持C++高效和对硬件的控制能力,即不牺牲性能。
- 支持现代静态分析技术,方便开发时静态检查。
- 明确类型的内存访问语义,帮助工具和程序员理解代码。
- 深入洞察程序行为,尽早发现缺陷。
- 基于标准,跨平台且开源,方便社区使用和贡献。
- 支持主流编译器和操作系统,涵盖 MSVC、Clang、GCC,Linux、Windows、macOS。
这段内容是在说 这些类型设计的具体目标,主要包括:
- 内存安全
这些类型确保 边界安全(bounds-safety),也就是说不会越界访问。同时还有机制避免悬空指针(dangling pointers)等问题。
目的是取代非标准注解(如 Microsoft 的 SAL),用更现代和强类型的方式保证安全。 - 类型安全
防止不安全的类型转换,保证类型的一致性,避免因为类型错误引发的Bug。 - 效率
设计做到零开销(zero-overhead),也就是说它们的性能和写手写检查代码差不多,甚至更低开销。
相比传统不安全代码,开销尽量小,做到安全和性能兼顾。 - 抽象
将关注点分离:数据访问(view)和数据存储(container)分开,这样更灵活、清晰,便于维护和扩展。
总结来说,就是做一个既安全又高效的现代C++类型体系,帮助开发者写出健壮又性能优的代码
“注解”(Annotation)在编程中一般指的是给代码元素(比如函数、变量、类型)添加的额外信息或标记,用来辅助编译器、工具或开发者理解代码的用途、限制或行为。
具体来说:
- 作用:
注解帮助工具进行静态分析、安全检查、代码生成或文档生成。
比如告诉编译器某个指针参数不能为空,或者某个函数不会修改传入的参数。 - 形式:
注解可以是特殊的注释、宏定义,或者语言本身支持的属性(attribute)。
比如微软的SAL(Source Annotation Language)就是用一组宏注解函数参数,提示参数的行为。 - 例子:
这些注解不是代码逻辑,只是给静态分析工具看的,帮助发现潜在错误。_In_ int* ptr // 表示 ptr 是输入参数,不会被修改 _Out_ int* ptr // 表示 ptr 是输出参数,函数会写入数据 _In_reads_(count) int* arr // 表示 arr 是输入数组,有 count 个元素
- 为什么要替换注解:
传统注解依赖宏定义和工具,语法不够现代和严谨,容易写错或忽略。
新的类型安全设计用更强类型和接口来替代注解,直接从类型系统保证安全,效果更好,编译期错误更多。
总结的 array_view<ValueType, Extents...>
特性挺全面的,补充一点:
- 核心思想:
array_view
不拥有数据,只是对已有连续内存区域的轻量“视图”。 - 安全性:避免了裸指针直接操作多元素数组时容易出现的越界问题。
- 高效:存储指针和长度(或编译时长度),复制开销很小,是值类型。
- 灵活:支持动态长度和固定长度(编译时确定),固定长度版本在编译期可优化更多。
使用场景举例:
void process(array_view<int> data) {
for (int x : data) {
// 处理每个元素
}
}
// 调用时:
int arr[10];
process(arr); // 传入动态长度视图
process(array_view<int, 10>{arr}); // 传入固定长度视图
这样既方便又安全,还能明确约束长度,减少出错可能。
关于 array_view
这一部分可以总结如下:
- 存储开销极小:
- 对于动态长度的
array_view
,它内部大致就是一个指针T*
和一个大小size_type
,保存指向数据的指针和长度。 - 对于固定长度(比如长度是模板参数),只需保存一个指针,长度在编译期已知,这样就没有额外开销(zero overhead)。
- 对于动态长度的
- 不进行任何内存分配,
array_view
只是对现有内存区域的“观察视图”,不拥有内存。 - 长度不可变:构造时确定后,长度不能变。
- 访问总是带边界检查,一旦越界访问,程序立即失败(fail-fast),这有助于快速发现错误。
结合提到的 CppCon 示例里的read
函数代码,其实用裸指针写类似代码时极易出错,比如:
packet += sizeof(Foo);
Bar* bar = (Bar*)packet;
// 这里假设packet有效且足够大,但实际上没边界检查,可能越界
packet += sizeof(Bar);
FuzzBuzz* fuzzbuzz = (FuzzBuzz*)packet;
// 如果前面判断有误,fuzzbuzz指针可能越界或非法
如果用 array_view
,可以替代裸指针和长度传递,更安全:
int read(array_view<char> packet) {
size_t needed = sizeof(Foo) + sizeof(Bar) + sizeof(FuzzBuzz);
if (packet.size() < needed)
return -1;
Foo* foo = reinterpret_cast<Foo*>(packet.data());
// 安全使用 foo
auto barView = packet.subspan(sizeof(Foo));
Bar* bar = reinterpret_cast<Bar*>(barView.data());
// 安全使用 bar
auto fuzzbuzzView = packet.subspan(sizeof(Foo) + sizeof(Bar));
FuzzBuzz* fuzzbuzz = reinterpret_cast<FuzzBuzz*>(fuzzbuzzView.data());
// 安全使用 fuzzbuzz
}
这样不仅写得更清晰,边界也受到了严格保障。
#include <iostream>
#include <span>
#include <cstring> // 用于模拟填充数据
struct Foo {
int someEntry;
};
struct Bar {
double someField;
};
struct FuzzBuzz {
char anotherField[8];
};
int read(std::span<char> packet) {
size_t needed = sizeof(Foo) + sizeof(Bar) + sizeof(FuzzBuzz);
if (packet.size() < needed) return -1;
Foo* foo = reinterpret_cast<Foo*>(packet.data());
// 安全使用 foo
std::cout << "Foo.someEntry = " << foo->someEntry << "\n";
std::span<char> barSpan = packet.subspan(sizeof(Foo));
Bar* bar = reinterpret_cast<Bar*>(barSpan.data());
// 安全使用 bar
std::cout << "Bar.someField = " << bar->someField << "\n";
std::span<char> fuzzbuzzSpan = packet.subspan(sizeof(Foo) + sizeof(Bar));
FuzzBuzz* fuzzbuzz = reinterpret_cast<FuzzBuzz*>(fuzzbuzzSpan.data());
// 安全使用 fuzzbuzz
std::cout << "FuzzBuzz.anotherField = ";
for (char c : fuzzbuzz->anotherField) std::cout << c;
std::cout << "\n";
return 0;
}
int main() {
// 模拟一块连续内存,大小刚好装得下三个结构体
alignas(Foo) char buffer[sizeof(Foo) + sizeof(Bar) + sizeof(FuzzBuzz)] = {};
// 在 buffer 中初始化三个结构体的内存
Foo* foo = reinterpret_cast<Foo*>(buffer);
foo->someEntry = 42;
Bar* bar = reinterpret_cast<Bar*>(buffer + sizeof(Foo));
bar->someField = 3.14159;
FuzzBuzz* fuzzbuzz = reinterpret_cast<FuzzBuzz*>(buffer + sizeof(Foo) + sizeof(Bar));
std::memcpy(fuzzbuzz->anotherField, "ABCDEFGH", 8);
// 调用 read 函数
int ret = read(std::span<char>(buffer, sizeof(buffer)));
if (ret != 0) {
std::cerr << "Packet too small!\n";
}
return 0;
}
Safety Features(安全特性)
- 安全类型转换:
array_view<int>
可以隐式转换成array_view<const int>
(允许增加 const 限定,安全)。array_view<int>
不能转换成array_view<short>
(类型不兼容,编译失败)。array_view<byte>
可以转换成array_view<T>
,只要T
是简单布局类型(例如 POD 类型),这提供了一定的灵活性。
- 方便地从已有容器构造:
- 可以从数组、
std::array
、std::vector
等自动推断大小,简化代码。
- 可以从数组、
固定大小的 array_view 提供更强的安全保障:
int arr[] = { 1, 2, 3, 4 };
array_view<int, 4> av4 = arr; // 完全匹配,安全,视图长度固定4
array_view<int, 2> av2 = arr; // 2 < 4,允许隐式转换(只看前两个元素)
av2 = av4; // ok,固定长度视图从更大的视图转到更小的视图允许
av4 = av2; // 错误,不能把小视图转换成大视图,编译失败
array_view<int> av_dyn = av2; // 固定长度到动态长度允许
av4 = av_dyn; // 动态长度转固定长度,运行时会检测越界并快速失败
固定长度视图利用类型系统防止越界访问,编译期就能捕获错误。
用例对比:传统字符串处理和 cstring_view
(字符串视图)
传统写 XML 代码:
void WriteXml(_In_reads_(cchText) PCSTR szText, size_t cchText)
{
if ((size_t)-1 == cchText) // 特殊值表示未传长度
cchText = strlen(szText);
while (cchText)
{
if (*szText == '&')
Write("&", sizeof("&"));
else
Write(szText, 1);
cchText--;
szText++;
}
}
存在问题:
- 需要特别处理未传长度情况,调用
strlen
。 - 手动处理字符指针,容易出错。
- 写法冗长。
用cstring_view
改写后:
void WriteXml(cstring_view text)
{
for (auto c : text) // 自动边界检查,无法越界
{
if (c == '&')
Write(ensure_z("&"));
else
Write({&c, 1});
}
}
优势:
- 不需要
strlen
特殊处理。 - 使用范围 for 循环,简洁且安全。
- 访问时有边界检查,减少安全风险。
- 代码更易读,意图明确。
总结
array_view
、cstring_view
等视图类型,通过类型系统和运行时检查,实现更安全的内存访问和边界保护。- 这些类型可以有效防止越界访问、非法类型转换和未初始化内存使用。
- 现代 C++ 代码借助这些类型,能写出更简洁、可维护且安全的代码。
string_view<CharType, Extent>
的核心特点
- 视图(View)概念:它是对一段连续字符序列的“视图”,本身不拥有字符数据,只是“指针 + 长度”的组合。
- 轻量级:复制和移动操作都很便宜,属于值类型(value type)。
- 长度灵活:长度可以在编译时固定(固定长度)或运行时指定(动态长度)。
- 无内存分配:不会分配新内存,只是现有字符数据的一个窗口。
- 长度不可变:构造后长度不可更改,保证视图的稳定性。
- 访问安全:所有访问都带有边界检查,防止越界。
- 快速失败:一旦发生访问越界等错误,程序会快速失败(fail-fast),提高安全性。
简单来说,string_view
是用来安全、高效地“观察”一段字符串内容,而不拷贝、不拥有数据,还能避免常见的越界错误。
string_view<CharType, Extent>
的本质
- 本质上是
array_view
的别名
具体定义如下:template <class CharT, size_t Extent = dynamic_range> using basic_string_view = array_view<array_view_options<CharT, unsigned short>, Extent>;
- 封装了字符数组的安全视图,继承了
array_view
的所有特性,比如“指针+长度”、边界检查和无额外分配。 - 提供了方便的别名,针对常用字符类型和常量类型:
string_view
—— 对char
类型的字符串视图cstring_view
—— 对const char
类型的字符串视图(只读)wstring_view
—— 对wchar_t
类型的宽字符字符串视图cwstring_view
—— 对const wchar_t
类型的宽字符字符串视图(只读)
简而言之,string_view
其实是基于array_view
实现的,更专注于字符串的安全、高效访问,且有多种别名方便不同字符类型使用。
string_view<CharType, Extent>
的特点与用法
- 字符串操作成为自由函数
诸如find
、compare
、trim
等字符串相关的操作,不是成员函数,而是单独的函数,作用于string_view
,这样设计更灵活,减少类接口复杂度。 - 当前的 GSL(Guideline Support Library)实现还需补充这些函数,比如添加字符串相关的自由函数,完善功能。
- 对零终止(null-termination)不敏感
string_view
并不要求字符串必须以'\0'
结尾,这让它既可以表示传统 C 字符串,也可以表示任意的字符串切片。 - 初始化时需要显式表示零终止字符串
如果想从以'\0'
结尾的 C 字符串初始化string_view
,需要用类似ensure_z
这样的辅助函数,来告诉编译器这是一个零终止的字符串,从而正确初始化:void f(const char* s) { string_view sv = ensure_z(s); // 明确告诉是零终止字符串,初始化正确 ... }
- 优点
这种方式增加了源码的信息表达,代码更明确,避免隐式假设,提升安全性和可维护性。
#include <string_view>
#include <cstring> // 用于模拟填充数据
// 假设 string_view 是 gsl::basic_string_view<char>
std::string_view ensure_z(const char* s) {
// 如果传入指针为空,返回空视图
if (s == nullptr) {
return std::string_view{};
}
// 计算字符串长度(不包含 '\0')
size_t len = std::strlen(s);
// 返回从指针和长度构造的 string_view
return std::string_view{s, len};
}
void f(const char* s) {
std::string_view sv = ensure_z(s); // 明确告诉是零终止字符串,初始化正确
}
int main() {
f("test");
return 0;
}
array_view
(或者类似的视图类型)常见的“提取子视图”功能,具体含义和用途如下:
提取子视图(sub-views)函数说明
- 固定长度版(模板参数编译时指定长度)
array_view<T, Count> first<Count>() const; // 返回视图的前 Count 个元素 array_view<T, Count> last<Count>() const; // 返回视图的后 Count 个元素 array_view<T, Count> sub<Offset, Count>() const; // 返回从 Offset 开始长度为 Count 的子视图
- 动态长度版(运行时指定长度)
array_view<T> first(size_type count) const; // 返回前 count 个元素的子视图 array_view<T> last(size_type count) const; // 返回后 count 个元素的子视图 array_view<T> sub(size_type offset, size_type count) const; // 从 offset 开始,取 count 个元素的子视图
- 通用的截取函数
这个通常用于多维数组视图,也适合一维情况,截取从array_view<T> section(size_type offset, size_type count) const;
offset
开始的count
个元素的子视图。
作用和优点
- 方便对数组或序列的一部分进行操作,无需拷贝数据。
- 子视图仍然是轻量的,不涉及内存分配。
- 编译时固定长度的版本能在类型层面确保安全性。
- 运行时版本更灵活,适合长度不固定的情况。
- 适合函数式编程风格,链式操作也很自然。
例如:
array_view<int> av = /* 假设长度是 10 */;
auto first_three = av.first(3); // 取前3个元素
auto last_two = av.last(2); // 取最后2个元素
auto middle = av.sub(3, 4); // 从索引3开始取4个元素
#include <span>
#include <cstring> // 用于模拟填充数据
int main() {
int arr[] = {1, 2, 3, 3, 4, 5, 6, 7, 9, 10}; /* 假设长度是 10 */
std::span<int> av(arr);
auto first_three = av.first(3); // 取前3个元素
auto last_two = av.last(2); // 取最后2个元素
auto middle = av.subspan(3, 4); // 从索引3开始取4个元素
return 0;
}
这段话讲的是 array_view
(或类似的视图类型)如何和旧代码(legacy code)配合使用的细节。
- 构造函数支持
(T*, size_type)
可以用一个裸指针和长度来构造视图对象。这种方式适合与老接口交互(比如C语言接口或旧的二进制接口 ABI),因为它们通常就是用裸指针和长度来传递数据。 - 提供访问底层裸指针的接口
T* data()
这样可以方便地把数据传给只接受裸指针的旧函数。 - 使用这些裸指针操作是“信任我”的行为
因为绕过了视图的边界检查,操作时不会自动安全检查。
所以需要用[[suppress(bounds)]]
之类的机制告诉静态分析器,这里是有意识地绕过了检查,程序员负责保证安全。
总结:
这种设计保证了现代安全视图类型能和旧代码兼容,但同时提醒开发者,使用裸指针要小心,可能会带来安全风险,需要手动确认安全性。
段话描述了 array_view
(或类似的视图类型)相较于提案 N3851 的一些主要区别。
主要改动:
- 允许固定每个维度的大小
- 这意味着可以在某些情况下指定特定维度的大小,并在编译时确定视图的大小,从而提高类型安全性和效率。
- 增加了从字节表示和到字节表示的转换
- 新的操作如
as_bytes()
和as_array_view()
允许将视图转换为字节流表示或从字节流恢复视图。这对于需要按字节操作数据的场景非常有用。
- 新的操作如
- 增加了更多的构造函数,支持便捷的“插入式使用”
- 提供了更多的构造函数选项,可以更方便地将不同类型的数据容器转换为
array_view
类型,使得array_view
更易于与现有代码和容器集成。
- 提供了更多的构造函数选项,可以更方便地将不同类型的数据容器转换为
- 在元素和字节之间描述长度
- 新的设计允许通过
length()
和bytes()
函数分别返回元素数量和字节数。这对于不同的内存操作非常有用,特别是在处理原始内存块时,开发者需要能够明确知道数据的大小和长度。
- 新的设计允许通过
- 允许指定用于测量/索引的大小类型
- 可以为视图的索引操作指定一个特定的大小类型,从而在某些特定情况下,精确地控制如何表示和索引数据。
- 增加了更多的“切片和切分”操作
- 新增的
first()
、last()
和sub()
等函数可以对视图进行分割,支持更灵活的数据操作和提取子视图的功能。
- 新增的
总结:
这些更改使得 array_view
类型更加通用、灵活和高效,可以更方便地与现有的代码进行集成,同时提供了更多的内存和数据操作功能,尤其是在处理多维数据或需要更细粒度控制的场景中。
描述了 string_view
在 Library Fundamentals TS 提案与其在 GSL(Guideline Support Library)中的实现之间的差异。以下是这些差异的详细解释:
主要差异:
string_view
是array_view<CharType...>
的类型别名string_view
被实现为array_view
的一个特化版本。它是一个对字符数组的轻量级视图,提供了对字符数据的无缝访问。
- 增加了固定长度的能力
string_view
在某些情况下可以在编译时固定其长度。这是通过引入编译时已知的长度参数来实现的。这样可以提高类型安全性,并允许在编译时捕捉潜在的错误。
- 增加了字节表示的转换功能
string_view
提供了与字节表示的转换(例如:as_bytes()
)功能。允许将字符串视图转换为字节流,或者将字节流转换为字符串视图。这对于低级别的内存操作或需要按字节处理的场景非常有用。
- 同时描述元素和字节的长度
string_view
可以提供字符串的长度信息,不仅可以用元素的数量来表示长度,还可以用字节数来描述。这对于一些涉及原始内存或低级内存操作的操作非常方便。
- 允许为度量/索引指定大小类型
string_view
允许开发者为字符串的度量或索引操作指定一个特定的大小类型。这使得开发者可以在某些特定情境下更加灵活地控制索引操作的行为。
- 支持可变或不可变字符的字符串视图
string_view
可以用来处理指向可变字符数组或者不可变字符数组的视图。这使得它既可以用于处理不可修改的字符串(例如常量字符串),也可以用于处理需要修改的字符串数据。
- 要求从零结尾字符串显式构造
string_view
必须显式地从零结尾的字符串构造。这是因为零结尾的字符串是 C 风格字符串的常见表示方式,而string_view
需要知道何时结束,因此不允许隐式的转换。
- 具有作为自由函数的特定于字符串的函数
string_view
提供了许多与字符串处理相关的自由函数(例如:查找、比较、裁剪等),使得string_view
操作更为简洁和易用。
总结:
这些差异使得 string_view
在 Library Fundamentals TS 提案和 GSL 实现之间的灵活性和功能有所不同。GSL 版本提供了更多的控制选项(例如,可以支持固定长度、字节转换等),同时提高了与其他类型和底层数据操作的兼容性。
Early Lessons from Usage
1. Easy replacement at call sites – nearly always “just add braces”
- 意思:将常规的指针和长度组合(如
foo(p, len)
)替换为array_view
类型时,通常只需要在调用处添加大括号{}
,即使没有做太大改动,也能顺利工作。- 举例:
foo(p, len); // 传统做法,传递指针和长度 foo({p, len}); // 使用 array_view 替代,自动推导类型并明确表达长度
- 解释:通过添加大括号
{}
,就将一个指针和长度的组合变成了一个array_view
类型的对象,编译器会根据上下文推导出类型。这种方式简化了代码修改。
- 举例:
2. Required little change inside callees besides length calculations
- 意思:在函数内部,除了长度计算之外,几乎不需要对代码进行太多修改。
- 举例:
- 传统做法:
for (UINT i = 0; i < len; ++i) { // 处理数据 }
- 使用
array_view
后:for (UINT i = 0; i < p.length(); ++i) { // 处理数据 }
- 传统做法:
- 解释:当你将原来的指针和长度组合替换为
array_view
时,唯一需要修改的通常是len
这个变量,因为你不再直接使用长度,而是调用p.length()
获取视图的长度。
- 举例:
3. bytes/elements difference makes code clearer to read
- 意思:在操作内存时,区分字节和元素的长度,使得代码更加清晰和易于理解。
- 解释:
array_view
允许你明确地知道你正在处理的是字节数据还是按元素计算的数据。通过提供两种长度信息(字节数和元素数),代码更加明确和自文档化。例如,array_view
可以描述一个视图的元素数量或字节数量,避免了混淆。
- 解释:
4. Need to wrap standard and common libs to understand array_view<byte>
(at the least)
- 意思:要让标准库和常用库理解
array_view<byte>
,可能需要进行适配或包装。- 解释:大多数现有的标准库函数(如
memcpy()
、memset()
、ZeroMemory()
、CopyMemory()
等)通常依赖于裸指针和长度参数。然而,array_view
提供了一个更加类型安全和自文档化的接口,但这些库函数并不直接支持array_view
类型。因此,为了与这些标准库函数兼容,可能需要将array_view<byte>
转换为裸指针或手动包装。
- 解释:大多数现有的标准库函数(如
Performance: Key Insights
这段话描述了如何通过类型系统提供的保证和优化来提升性能,尤其是在使用像 array_view
这样的类型时,如何通过编译器优化来减少性能开销。
1. Optimization can leverage guarantees provided by type system
- 意思:优化可以利用类型系统提供的保证。类型系统通过定义变量和类型的行为,使得编译器能做更多的优化。
- 举例:通过使用
const
修饰符,我们告诉编译器某些变量不会改变。编译器可以利用这些信息进行更高效的优化,比如在编译时就知道某些变量值不会发生变化,避免重复计算。
- 举例:通过使用
2. Make it clear to the optimizer you are a simple, safe type
- 意思:通过明确声明你的类型是简单且安全的,编译器可以做出更多优化。对于
array_view
,它是一个简单的视图类型,表示一个指向元素的指针以及元素的长度。因为它不包含复杂的状态或动态分配,编译器可以更高效地进行优化。
3. Range-check optimizations are important and do-able
- 意思:范围检查优化是可能实现的,并且对于性能非常重要。
- 解释:传统的数组越界检查会带来性能开销,但对于
array_view
类型来说,编译器可以识别这些范围检查并进行优化。例如,可以通过提前消除冗余的范围检查来减少不必要的计算。
- 解释:传统的数组越界检查会带来性能开销,但对于
4. Hoisting and elimination optimizations
- 意思:范围检查的优化包括“提升”(hoisting)和“消除”(elimination)。提升是将范围检查移到循环外面,从而避免在循环内部重复检查。消除是完全去掉不必要的范围检查(如果编译器可以证明它们是冗余的)。
5. MSVC already knows how to do efficient range-checking for .NET Native compilation
- 意思:MSVC 已经知道如何为 .NET Native 编译进行高效的范围检查,并且可以使用
RNGCHK()
进行优化。并且,使用这种方法时,不需要像传统的垃圾回收、框架或运行时 DLL 那样的开销。
Example 1: Optimization with Simple C Code
typedef int my_array[9];
my_array glob;
void f(my_array a) {
// a 是一个数组,编译器知道它的地址在这里不能改变
for (int i = 0; i < len; ++i)
a[i] = glob[i]; // 这行代码,编译器知道 glob 的地址不会改变
// 循环是单调递增的
}
- 解释:这里,
a
和glob
都是固定数组类型。编译器知道a
和glob
的指针不会改变,因此它可以将这些指针加载到寄存器中,并且循环的计算可以进行强度归约(即减少不必要的计算)。 - 优化效果:这种简单的代码在编译时就能进行大量优化,最终会减少机器指令的数量,从而提高性能。
Example 2: Optimization with array_view
int arr[9] = { ... };
const array_view<int, 9> glob = arr; // const 表示 glob 的 int* 成员不会改变
void f(array_view<int, 9> p) {
// p 包含一个单一的 int*,编译器知道从这里开始,int* 的值不会改变
for (int i = 0; i < p.length(); ++i)
a[i] = glob[i]; // 这行代码会进行范围检查
// 循环依然具有和之前相同的属性
}
- 解释:这里使用了
array_view<int, 9>
来替代传统的固定数组。array_view
在内部使用一个指针来引用数据,并且通过length()
提供数组的长度。由于array_view
是const
类型,编译器可以确保指针不会发生变化。 - 优化:
p
的指针成员可以通过寄存器传递,并且只加载一次。- 循环的强度归约可以继续进行,编译器可以优化循环。
- 范围检查优化:即使代码中有范围检查,编译器可以通过证明该检查是冗余的来将其消除(消除掉无用的检查)。
- 如果编译器无法消除检查,它可能会将范围检查提取到循环外部,从而避免在每次迭代时进行检查。
结论
通过这些优化,使用 array_view
和类似的简单类型可以显著提高性能。编译器利用类型系统的保证进行优化,使得代码更加高效。特别是在范围检查、强度归约和指针优化方面,array_view
的使用能够让编译器更容易进行这些优化,同时保持类型安全。