VS 2017 RTM 关于STL 的修复

 

[原文发表地址]VS 2017 RTM 关于STL 的修复

[原文发表时间] 2017/2/6 9:20AM

VS 2017 RTM 版本很快就要发布了。 目前VS 2017 RC 已经投入使用,并且包含了我们在这里描述的所有改变 – 请尝试在IDE 的help >Send Feedback >Report A Problem (或者 Provide A Suggestion) 提交您的反馈信息。

关于STL 在VS2015 update 3 和 VS 2017 RTM之间的改变, 这是第三篇也是最后一篇博客。 在第一篇博客中(关于2017 Preview 4),我们详细地阐述了2015 和2017 版本是如何实现二进制兼容问题的。 在第二篇博客中(关于VS 2017 Preview 5),我们列举了那些添加到编译器和STL 的模块。(从此之后,我们已经实现了P0504R0中新引入的 in_place_t/in_place_type_t<T>/in_place_index_t<I> 和P0510R0 中抛弃的数组, 引用以及不完整类型变量。)

Vector 修改:

我们已经修改了vector<T> 的成员函数, 修复了许多运行时和性能方面的缺陷。

*修复了一些别名错误。 例如, 尽管标准支持v.emplace_back(v[0]),但是在运行过程却处理失当,还有v.push_back(v[0]),防止其产生有缺陷的代码等(”这个对象作用域在我们的内存块中吗”,一般而言,这往往是无效的)。在性能方面的修复是井井有条的。 因此凡是我们提到的都将是支持的。

为了防止别名错误,, 在没有别的选择的情况下,我们只能在栈上构造一个元素 (例如:emplace()有充足的能力构造这个元素,但是却不限于此)。(这儿有一个还没有被修复的缺陷, 它是非常晦涩难懂的---严格地说,我们还没有尝试用allocator的construct() 函数来处理此类对

象。)请注意我们的实现基于标准:在每一个成员函数中不允许出现别名错误的基础之上。--例如, 在多个元素同时插入的时候不允许出现别名错误,因此我们也不会去修复这种缺陷。

*修复异常处理准则 .  早在VS2010开始支持移动构造语意, 那时当我们重新分配容器时都会无条件地使用移动构造方式构造元素. 这确实显著提高了速度, 但是很遗憾,这样并不正确. 现在我们依照标准准则使用move_if_noexcept()模式,(无异常时使用移动构造)

例如,当我们调用push_back() 和 emplace_back() 的时候,它们需要重新分配。他们将会询问这个元素:“你需要在不抛出异常的情况下,使用移动构造函数吗?如果是的话,我将会采取移动构造。(它不会失败,并且它将非常迅速)。 除此之外,你需要拷贝构造吗? 如果是的话, 随后我将拷贝给你(可能有些缓慢, 但是将不会破坏完善的异常准则)。 另外,如果你仅仅只是在抛出有价值的异常信息的情况下使用移动构造函数, 我也将采用移动构造的方式,但是如果出现异常, 你将不会得到完善的EH 异常确认信息。” 现在, 基于一些模糊的异常信息, 所有Vector 的成员函数实现了标准文档规定的基本或者高级的EH异常信息确认。(第一个异常提出了值得反思的标准,这表明在当构造元素时, 仅仅是输入迭代程序的范围插入必须提供一个严格的信息确认, 如果不是孤注一掷,这基本是不可能实现的, 并且没有人知道曾经都做了哪些实现, 我们的实现提供了基本的信息确认: 我们反复提到的emplace_back(),rotate() 到相应的位置上, 如果emplace_back() 中的一个抛出异常, 我们可能需要放弃之前的内存块, 这种变化是可以看到的。 第二个异常是就POCCA/POCMA 分配而言, ”重载”proxy 对象(以及在其他容器中的标记节点))通常在这里很难避免内存越界,幸好std::allocator 不会触发重载)。

*淘汰了一些不必要的EH 逻辑。 例如, vector 的赋值运算符重载有一段没有意义 的try-catch 代码块。 它仅仅提供了基本的信息确认, 我们也可以通过合适的排列来达到这种目的。

*轻微地改善调试性能。 尽管这对我们而言,优先级不是很高(在不能使用优化器的条件下,我们所做的每一件事代价都是巨大的),我们尽可能地避免严重破坏调试性能。 在这种情况下, 当我们可以使用指针时, 在内部的实现过程中, 有时是完全没有必要使用迭代器。

*改善迭代器的有效性检查。例如,resize() 将不会认为结束迭代器是无效的操作。

*减少调用rotate() 函数从而改善性能。 例如,emplace(where, val) 在rotate()之后调用emplace_back()。 现在,在只有一种情况下,vector 调用rotate()(关于只输入迭代器的范围插入已经在前面阐述过了)

*固化权限控制。现在, helper 成员函数是私有的。(一般而言, 我们预留命名为_ugly的变量去实现新功能,因此公共的helper实际上并不是一个缺陷。)

*用allocators来优化性能。例如,用不常规的分配器移动构造函数来激活memmove()优化选项(以前我们使用make_move_iterator(), 这常常不能使用memmove()来进行优化), 现在我们将在VS 2017 update 1 中进行了进一步的改进, 在non-POCMA 不常规的情况下,移动赋值运算重载函数可以重新使用缓存。

注意这个修复本身引起了源码的重大改变。最常见的是,标准规定move_if_noexcept() 可以在某种场景下实例化拷贝构造函数。如果它不能被实例化的, 你的程序将无法编译。 或者, 我们可以尝试使用在标准模板中要求的其他操作符。例如:”N4618 23.2.3 [sequence.reqmts] 说明a.assign(i,j)要求: T 将会构造对象X , 并且赋值为*I” 为了提高性能,我们正在着力于充分利用*i的赋值运算。

警告修复:

这个编译器有一个详细的关于警告,包括警告等级以及压入/禁用/弹出的编译指令系统。编译器警告同样适用于用户代码和标准模板库的头文件。其他标准模板库实现禁用了所有“系统头文件”编译器警告,但我们是按照另一种不同原理。编译器警告的存在是为了警告一些不规范的行为,比如修改值的类型转换或是返回临时变量的引用等。这些行为同样需要考虑到性能问题, 无论这些性能问题是执行用户代码引起的或者是用户调用STL 函数模板执行过程引起的。

。显然,标准模板库不应该为自己的代码发出警告,但是我们认为禁用STL中所有头文件中所有警告是不可取的。

许多年来,STL 一直在清理/W4 /analyze, 并且通过大量的测试进行验证

(没有 /Wall, 这是不同的)。长期以来,我们把在标准模板库中的警告级别推到了3等级,进一步地禁用某些警告。虽然这能够使我们完美的地进行编译,但却忽视了一些有价值的警告提醒。

现在,按照新的方法我们已经对标准模板库进行了修复。首先,我们检测你是否正在使用/W3编译(或较弱级别,但你应该从来没有这样做)和/W4(或/Wall,但你是用你自己的,与标准模板库在技术上不支持)。当我们感觉到/W3(或较弱)时,标准模板库将其警告等级推到3(即以前的行为没有改变)。当我们感觉到/W4(或较强)时,标准模板库现在就将其警告等级推到4,那就意味着,4等级警告现在将应用到我们的代码中。此外,我们审计了所有我们个别警告禁止显示设置(包括产品和测试代码),消除了不必要的禁止以及使剩下的更有针对性(有时到各个函数或类)。在整个标准模板库中我们同样禁用了C4702警告(无法访问的代码);虽然此警告对用户有价值,但它是与优化级别相关的,而且我们相信,在标准模板库头文件中启动这个警告将会是弊大于利。我们使用两个内部的测试套,再加上libc++中的开源测试套,一起用验证我们不会针对自己的代码发出警告。

这对你来说意味着什么呢?如果你正在用W3编译(我们不推荐),你应该观察到没有什么大的变化。因为我们已经重新归整了这些警告,你可能已经观察到了几个新的警告,但这应该是相当罕见的。(当它们出现时,说明你正在使用当下的STL存在潜在隐患, 如果它们不是预料中的,报告一个缺陷)。

)如果你正在用/W4编译(我们支持),你应该看到一个由STL 头文件报告的警告错误, 这是用/WX 引起的源码的重大改变,这种改变是有意义的。毕竟,是你要求的4级警告,标准模板库现在也在遵循它。例如,依赖输入类型的STL 算法报告的关于各种中断和符号转化引起的警告。此外,现在通过输入类型被激活的非标准扩展将在标准模板库的头文件中触发警告。当发生这种情况时,你应该修复你的代码去避免警告(例如,通过更改你传给标准模板库的类型来纠正你的函数对象特征,等等)。然而,会有遗漏的出口。

首先,这个宏_STL_WARNING_LEVEL控制标准模板库是否推其警告级别至3级或4级。如前文所述,可以通过检查/W3或/W4自动化确定,但你可以通过定义宏的项目范围来覆盖这个。(只有3和4的值被允许,其他的会发出错误)。所以,如果你想在标准模板库推其警告级别至3级之前使用/W4编译,你可以请求标准模板库将其警告级别推至4级。

其次,这个宏_STL_WARNING_LEVEL(它将总是默认为空)可以通过定义项目范围来禁用整个标准模板库头文件的选择性警告。例如,定义它为4127 6326可以禁用”条件表达式是常数“和”一个常量和另一个常量的潜在比较“(我们应该早已清除了这些警告,这只是一个例子)。正确性修复和其它的一些改进:

*有些STL算法用const来定义他们的迭代器。源代码的重大改变:由于标准文件中的要求,我们需要将operator* 作为const来处理。

* 改进了basic_string 迭代器调试检查时的诊断器功能。

* basic_string的iterator-range-accepting函数针对(char *, char *)另外实现了重载,这个额外的重载现在已经被移除,因为它阻止了String.assign(“abc”,0)的编译。 (这不是源代码的重大修改; 调用旧的重载方法的代码现在会调用新的(Iterator,Iterator)重载来替代它)。

* basic_string 重载的范围包括append, assign, insert 和replace, 不再需要default的默认构造函数来分配。

* basic_string::c_str(), basic_string::data(), filesystem::path::c_str() 和locale::c_str() 现在可以用SAL注释来声明他们是以null来结束的。

* array::operator[]() 现在可以通过SAL注释来改善代码的警告分析 (注意:我们不会尝试让SAL注释进入整入STL,我们只会在一些个案上使用SAL注释)。

* condition_variable_any::wait_until 现在可以接受较低精度的时间点类型。

* stdext::make_checked_array_iterator的调试检查现在允许迭代器通过C++14的空向前迭代器的需求来做迭代比较。

* 引用C++工作文件的需求来改善了<random>中static_assert 消息的性能。

* replace_copy() 和 replace_copy_if() 中条件运算符的实现是错误的,错误的要求输入元素的类型和新值的类型来转换成为一个公共的类型。 现在,他们已经被更正为用if-else来实现,以避免被要求转换。(需要在输出迭代器中分别写入输入的元素的类型和新值的类型)

*STL现在比较重视空的fancy指针,但并不试图去引用他们,哪怕一瞬间。(vector修复部分)

* 各种STL成员函数 (例如: allocator::allocate(), vector::resize()) 已经被标记为_CRT_GUARDOVERFLOW。 当使用/sdl编译选项时,调用函数前检查整数溢出会被解释为 __declspec(guard(overflow))。

* 在<random>中independent_bits_engine的任务是在构造和生成种子时包装一个基础的engine (N4618 26.6.1.5 [rand.req.adapt]/5, /8) ,但是他们会返回不同的result_types. 例如: independent_bits_engine可以通过运行32-bit mt19937来生成uint64_t, 这将触发截断警告。这种编译是正确的,因为这是一个物理的数据截断丢失 – 然而,为了完成标准文档中它的任务, 我们添加了 static_cast来使它的编译不会受到编译器的影响。

* 修复了std::variant中的一个缺陷, 当编译 std::get<T>(v) 且变量v不是一个唯一的可替代的类型,它会引起编译器填满所有的可用的堆空间并且报错退出。例如:当v 是 std::variant<int, int>时的 std::get<int>(v) 或std::get<char>(v) 。

运行时性能的提高:

* basic_string 中移除了构造、赋值,使交换的性能提高了三倍。通过使Traits为std::char_traits, 分配器的类型不能是fancy指针,而且一般情况下没有别的可能。因为我们一般使用移除/交换方法而不是操作个别的basic_string数据成员。

*basic_string::find(character) 现在只在查找一个字符的情况下可以使用,而不是一个长度为1的字符串。

*basic_string::reserve不再做重复的范围检查。

* 在字符串收缩的情况下, 所有的basic_string函数中分配,移除是唯一的储存方式。

* stable_partition不再执行self-move-assignment操作。此外,它还会在输入区间的两端跳过已经分区的元素。

* shuffle和random_shuffle不再执行self-move-assignment。

* 分配临时空间的算法(stable_partition, inplace_merge, stable_sort) 不再传递和基址完全相同的复本和大小相同的临时空间.

* filesystem::last_write_time(path, time) 是对问题1的磁盘操作而不是问题2。

* std::variant中的visit()方法的性能有小幅的提高 :  已经分派到合适的visit函数后就不再去验证所有的变量是否有valueless_by_exception(), 因为std::visit()在分派前已经保证具有这个属有。 std::visit()的性能的提高几乎是可以忽略不计的,但是却大大降低了visitation生成的代码的大小。

编译器的吞吐量做出的改进

* 源代码的重大改变: <memory> 功能不再被仅仅在STL的内部来使用。(uninitialized_copy, uninitialized_copy_n, uninitialized_fill, raw_storage_iterator, and auto_ptr) 现在只会在<memory>头文件中会出现。

* 会集中对STL算法中的迭代器进行调试检查。