对 gcc 使用 -march 开关不会对 运行 时间速度产生影响
using -march switch for gcc does not make a difference in terms of run-time speed
我使用 GCC 11.1 构建了一个小程序(~1000 LOC),运行 它在启用和不启用 -march=native
的情况下进行了多次迭代,但总体而言 没有 程序执行时间的差异(以毫秒为单位)。但为什么?因为它是单线程的?还是我的石器时代硬件(第一代 i5,Westmere 微架构,没有 AVX 东西)功能不够?
我的 Makefile 中的几行:
CXX = g++
CXXFLAGS = -c -std=c++20 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -O3 -march=native -flto
LDFLAGS = -O3 -flto
Here (Compiler Explorer) 是 GCC 不为其生成 SSE 指令的程序的自由函数:
[[ nodiscard ]] size_t
tokenize_fast( const std::string_view inputStr, const std::span< std::string_view > foundTokens_OUT,
const size_t expectedTokenCount ) noexcept
{
size_t foundTokensCount { };
if ( inputStr.empty( ) ) [[ unlikely ]]
{
return foundTokensCount = 0;
}
static constexpr std::string_view delimiter { " \t" };
size_t start { inputStr.find_first_not_of( delimiter ) };
size_t end { };
for ( size_t idx { }; start != std::string_view::npos && foundTokensCount < expectedTokenCount; ++idx )
{
end = inputStr.find_first_of( delimiter, start );
foundTokens_OUT[ idx ] = inputStr.substr( start, end - start );
++foundTokensCount;
start = inputStr.find_first_not_of( delimiter, end );
}
if ( start != std::string_view::npos )
{
return std::numeric_limits<size_t>::max( );
}
return foundTokensCount;
}
我想知道为什么?可能是因为无法向量化此类代码?
此外,我想提及的另一件事是最终可执行文件的大小根本没有改变,我什至尝试了 -march=westmere
和 -march=alderlake
来查看大小是否有任何差异但是GCC 以相同的大小生成它。
我认为您也应该将 -march=native
指定为 LDFLAGS 的一部分,因此 -flto
是针对同一台机器的。
但您的 code-gen 似乎尊重您指定的拱门,因为您说 -march=alderlake
使代码因 SIGILL 而崩溃,可能是在矢量指令的 AVX 编码上。
很有可能 -mtune=generic
做出与 -march=native
相同的调优决定,而且除了 SSE2 之外,没有什么能从中受益。您的 CPU 支持 SSE4.2 和 popcnt,但 x86-64 的基线已经是 SSE2,相同的矢量宽度只是缺少一些指令,特别是对于 dword 和 qword 元素大小(如打包 min/max)。
GCC/clang 无法 auto-vectorize 搜索循环(仅在第一次迭代之前在运行时已知 trip-count 的循环),因此 inputStr.find_first_of
要么编译为one-byte-at-a-time 搜索,或调用 memchr
,它只受益于 SSE2,但可以基于 CPU 功能进行动态调度,因为它在共享库中。
(Glibc 使用“解析器”函数重载动态链接过程,该函数决定 memchr
的哪个实现在当前机器上最好,SSE2 或 AVX2。两个版本都是 hand-written asm ,例如 the SSE2 version's source。像 strstr
这样的几个函数有 SSE4.2 版本,你 CPU 可以利用,但这个选择不依赖于 -march
compile-time设置,纯run-time动态链接器+glibc。)
如果你想查看你的程序大部分时间花在哪里,请使用 perf record ./a.out
/ perf report -Mintel
(默认是 AT&T 语法反汇编;我更喜欢 Intel)。
如果它在库函数中,不同的调整选项和可用的新指令可能对您的主代码没有帮助。如果它在你的程序中,而不是库中,那么显然 x86-64 的基线 instruction-set 和“通用”调整选项很好,或者 GCC 不知道如何使用 SSSE3 / SSE4.x 用于您的代码。
我没有仔细查看您的代码在做什么,以了解可能进行哪些手动矢量化。
我使用 GCC 11.1 构建了一个小程序(~1000 LOC),运行 它在启用和不启用 -march=native
的情况下进行了多次迭代,但总体而言 没有 程序执行时间的差异(以毫秒为单位)。但为什么?因为它是单线程的?还是我的石器时代硬件(第一代 i5,Westmere 微架构,没有 AVX 东西)功能不够?
我的 Makefile 中的几行:
CXX = g++
CXXFLAGS = -c -std=c++20 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -O3 -march=native -flto
LDFLAGS = -O3 -flto
Here (Compiler Explorer) 是 GCC 不为其生成 SSE 指令的程序的自由函数:
[[ nodiscard ]] size_t
tokenize_fast( const std::string_view inputStr, const std::span< std::string_view > foundTokens_OUT,
const size_t expectedTokenCount ) noexcept
{
size_t foundTokensCount { };
if ( inputStr.empty( ) ) [[ unlikely ]]
{
return foundTokensCount = 0;
}
static constexpr std::string_view delimiter { " \t" };
size_t start { inputStr.find_first_not_of( delimiter ) };
size_t end { };
for ( size_t idx { }; start != std::string_view::npos && foundTokensCount < expectedTokenCount; ++idx )
{
end = inputStr.find_first_of( delimiter, start );
foundTokens_OUT[ idx ] = inputStr.substr( start, end - start );
++foundTokensCount;
start = inputStr.find_first_not_of( delimiter, end );
}
if ( start != std::string_view::npos )
{
return std::numeric_limits<size_t>::max( );
}
return foundTokensCount;
}
我想知道为什么?可能是因为无法向量化此类代码?
此外,我想提及的另一件事是最终可执行文件的大小根本没有改变,我什至尝试了 -march=westmere
和 -march=alderlake
来查看大小是否有任何差异但是GCC 以相同的大小生成它。
我认为您也应该将 -march=native
指定为 LDFLAGS 的一部分,因此 -flto
是针对同一台机器的。
但您的 code-gen 似乎尊重您指定的拱门,因为您说 -march=alderlake
使代码因 SIGILL 而崩溃,可能是在矢量指令的 AVX 编码上。
很有可能 -mtune=generic
做出与 -march=native
相同的调优决定,而且除了 SSE2 之外,没有什么能从中受益。您的 CPU 支持 SSE4.2 和 popcnt,但 x86-64 的基线已经是 SSE2,相同的矢量宽度只是缺少一些指令,特别是对于 dword 和 qword 元素大小(如打包 min/max)。
GCC/clang 无法 auto-vectorize 搜索循环(仅在第一次迭代之前在运行时已知 trip-count 的循环),因此 inputStr.find_first_of
要么编译为one-byte-at-a-time 搜索,或调用 memchr
,它只受益于 SSE2,但可以基于 CPU 功能进行动态调度,因为它在共享库中。
(Glibc 使用“解析器”函数重载动态链接过程,该函数决定 memchr
的哪个实现在当前机器上最好,SSE2 或 AVX2。两个版本都是 hand-written asm ,例如 the SSE2 version's source。像 strstr
这样的几个函数有 SSE4.2 版本,你 CPU 可以利用,但这个选择不依赖于 -march
compile-time设置,纯run-time动态链接器+glibc。)
如果你想查看你的程序大部分时间花在哪里,请使用 perf record ./a.out
/ perf report -Mintel
(默认是 AT&T 语法反汇编;我更喜欢 Intel)。
如果它在库函数中,不同的调整选项和可用的新指令可能对您的主代码没有帮助。如果它在你的程序中,而不是库中,那么显然 x86-64 的基线 instruction-set 和“通用”调整选项很好,或者 GCC 不知道如何使用 SSSE3 / SSE4.x 用于您的代码。
我没有仔细查看您的代码在做什么,以了解可能进行哪些手动矢量化。