【STL】矢量化 MSVC STL 算法


本文介绍一下 STL 中的矢量化 MSVC STL 算法,可以理解为算法的优化版,借助 CPU 硬件提高代码效率。


目录

  • [1 概述](#1 概述)
  • [2 MSVC STL 中的自动矢量化](#2 MSVC STL 中的自动矢量化)
  • [3 MSVC STL 中的手动矢量化](#3 MSVC STL 中的手动矢量化)
  • [4 浮点类型的手动向量化算法](#4 浮点类型的手动向量化算法)
  • [5 备注](#5 备注)

1 概述

在特定条件下,MSVC 标准模板库(STL)中的算法可以在单个 CPU 核心上同时处理多个元素,而不是单独处理每个元素。此优化使用 CPU 提供的单个指令、多个数据(SIMD)指令,这是一种称为矢量化的技术。如果未应用此优化,则实现称为标量实现。
开启矢量化优化需要同时满足以下条件:

  1. 容器 / 区间内存连续

    • 支持:array、vector、basic_string、span、basic_string_view、原生内置数组;
    • 不支持:list、map 等非连续容器;
  2. 目标 CPU 支持对应元素类型所需的 SIMD 指令集,数值基础类型、简单运算通常都满足;

  3. 必须满足以下条件之一

    • 编译器自动向量化:编译器把标量循环代码编译生成向量化机器码;
    • 手动向量化:算法底层源码直接手写 SIMD 向量化逻辑;
      关键说明
  4. SIMD 向量化是 CPU 层面并行加速,并非多线程并行,只利用单核运算单元批量处理;

  5. 内存连续是硬性门槛,链表、树形容器内存分散,无法打包批量读取,永远只能标量执行;

  6. 基础数值(int / float / double)最容易触发向量化,自定义复杂结构体大多无法自动向量化;

2 MSVC STL 中的自动矢量化

自动矢量化规则对 STL 底层实现代码、用户自己编写的代码完全通用。

transform、reduce、accumulate 这类算法能从自动向量化优化中获得巨大性能提升。
补充说明

  1. /arch 编译参数用来指定 CPU 指令集(SSE、AVX、AVX2、AVX512等),编译器根据该参数生成对应 SIMD 向量化指令;
  2. 自动向量化:编译器识别循环标量逻辑,自动批量打包多个元素并行计算,无需手动写 SIMD intrinsics;
  3. 适用算法特点:批量遍历、简单算术运算、累加归约类操作,非常契合 SIMD 批量计算特性;
  4. 该优化仅作用于连续内存区间(vector / array / string 等),list、map 等碎片化容器无法触发;

3 MSVC STL 中的手动矢量化

部分算法内置手动向量化实现。该实现单独编译,依靠运行时 CPU 调度分支,因此仅在适配的 CPU 上才会启用优化逻辑。

  • 手动向量化算法通过模板元编程 判断元素类型是否适合向量化,仅对标准整数这类简单基础类型开启向量化路径
  • 开启手动向量化只会提升程序性能,不会带来负面效果。
  • 在项目中定义宏 _USE_STD_VECTOR_ALGORITHMS = 0 关闭手动矢量化。默认情况下,在 x64 和 x86 上启用手动矢量化算法,该宏默认值为 1。
  • 所有参与链接、使用标准算法的编译单元,必须统一 _USE_STD_VECTOR_ALGORITHMS 的取值。为保证一致性,建议在项目属性预处理器配置中设置(对应编译选项 /D),不要在源码内零散定义。

宏 _USE_STD_VECTOR_ALGORITHMS 控制以下算法的手动矢量化:

用途 标准算法
查找类 contains、contains_subrange、find、find_last、find_first_of、adjacent_find、search、search_n、find_end
统计类 count
匹配类 mismatch
交换复制 swap_ranges
替换 replace、replace_copy
删除去重 remove、remove_copy、unique、unique_copy
翻转旋转 reverse、reverse_copy、rotate
有序校验 is_sorted、is_sorted_until
集合比较 includes、lexicographical_compare、lexicographical_compare_three_way
最值 max、min、minmax、max_element、min_element、minmax_element

宏 _USE_STD_VECTOR_ALGORITHMS 控制以下项的手动矢量化:

  • basic_string 和 basic_string_view 成员:
    • find
    • rfind
    • find_first_of, find_first_not_of
    • find_last_of, find_last_not_of
  • bitset 字符串中的构造函数和 bitset::to_string

4 浮点类型的手动向量化算法

对浮点类型做向量化存在几点特殊问题:

  • 向量化会重新调整运算执行顺序,可能改变浮点计算结果精度;
  • 浮点数可能存在 NaN(非数值),NaN 的比较不满足传递性;
  • 浮点运算有可能触发硬件浮点异常;

标准库已经安全处理了前两个问题。仅有以下最值、有序校验类算法提供浮点专用手动向量化实现:

  • max_element
  • min_element
  • minmax_element
  • max
  • min
  • minmax
  • is_sorted
  • is_sorted_until

这类算法有两个特性,规避了浮点向量化风险:

  • 不会生成新浮点值,仅做已有元素的大小比较,运算顺序重排不会影响最终精度;

  • 作为排序 / 最值相关算法,输入序列不允许包含 NaN;
    通过宏 _USE_STD_VECTOR_FLOATING_ALGORITHMS 控制浮点向量化开关,设为 0 即可关闭。若 _USE_STD_VECTOR_ALGORITHMS 被设为 0,本宏会完全失效,无论取值如何都不会启用浮点向量化

  • 编译开启 /fp:except(启用浮点异常捕获)时,_USE_STD_FLOATING_ALGORITHMS 默认值为0,默认关闭浮点向量化。

  • 所有链接在一起、使用标准算法的编译单元,该宏取值必须保持统一。为保证全局一致性,建议在项目属性预处理器中统一配置(对应编译选项 / D),不要在源码中零散定义。

5 备注

  1. 手动向量化 VS 自动向量化
    • 自动向量化:编译器识别普通循环,自动生成 SIMD;用户、STL 代码通用;支持浮点、整数。
    • 手动向量化:微软工程师手写 SIMD 底层分支,单独编译,运行时检测 CPU 指令集;仅整数类型生效,覆盖大量查找、遍历类算法。
  2. 运行时 CPU 调度
    • 程序启动后检测 CPU 是否支持 AVX/AVX2 等指令集,匹配才走加速路径;
    • 老旧 CPU 自动降级为普通标量实现,无需分支判断代码;
  3. 宏使用强制规范
    • 若不同 .cpp 文件一个开、一个关,会出现符号冲突、链接报错,全局统一通过项目 /D 配置最稳妥。
  4. 生效内存前提
    • 即便开启宏,也仅对连续内存容器(vector / array / string)生效;
    • list / map 非连续内存无法 SIMD 批量加载,不会触发加速;
  5. 关闭场景
    • 极少数需要极致可复现性能、跨平台对齐测试的场景,才设置 _USE_STD_VECTOR_ALGORITHMS = 0,日常开发建议保持默认开启。