(libstdc++) std::function
涉及的 libstdc++ 源码文件:bits/std_function.h
。
印象: std::function
做了小对象优化,同时在避免使用虚函数(尽管它可以用继承和虚函数来实现)。
关于成员指针,见 Pointer to Member。
存储结构
首先看 std::function
的成员组成:
void swap(function& __x) noexcept
{
std::swap(_M_functor, __x._M_functor);
std::swap(_M_manager, __x._M_manager);
std::swap(_M_invoker, __x._M_invoker);
} // 模板太长了,成员定义很分散,抄起来很累;这个 swap 函数写的刚刚好
_M_functor
是一个 union 的 union(最外层的 union 是为了和 char
数组拼在一起方便取首地址,里面的 union 可以用来表示四种不同类型的指针):
union _Nocopy_types
{
void* _M_object;
const void* _M_const_object;
void (*_M_function_pointer)();
void (_Undefined_class::*_M_member_pointer)(); // 这个在 64 位环境的 gcc 里面是 16 个字节
};
_M_manager
和 _M_invoker
都是普通函数指针,是用来管理函数对象的调用和生命周期的。
函子(functor)存储:
- 如果函子是普通函数指针或者成员函数指针,则能直接存储(因为有上面的 union 来专门存储)。
- 如果函子是具有调用操作符的对象,则先考虑
_M_functor
占用的空间能不能用来本地分配(gcc 里成员函数指针占用 16 个字节,拿来做小对象优化还蛮好的)。这叫小对象优化(SOO)。 - 如果函子是具有调用操作符的对象,且本地空间不够,则会动态申请内存。
调用开销(不考虑创建函数对象时的开销)
我在 libstdc++ 实现里面没找到 std::function
对虚函数的使用?但是看到 std::function
调用有很多间接环节:
- 首先是读
_M_invoker
的值,然后才能调用其指向的函数,这涉及到读内存一次。 - 然后在跳转到的函数里,代码需要找到函子的地址。普通函数指针/成员函数指针会需要多访存一次。小对象优化的函子会将成员的首地址作为函子地址;大对象函子的
_M_functor
保存的值本身就是函子地址,也是还要访存一次。 - 如果使用
std::bind
等修改函数签名的技术,间接层数会更多。最好使用 lambda,方便编译器内联。
Tip
根据小对象优化,使用 lambda 表示纯函数(而不是函数指针),可以减少内存访问次数。
使用 google/benchmark 进行性能比较:g++ 14.0.1
CMakeLists.txt(需要先下载 google/benchmark):
cmake_minimum_required(VERSION 3.18)
project(bench LANGUAGES CXX)
add_executable(bench main.cpp)
target_link_libraries(bench benchmark::benchmark)
target_compile_options(bench PRIVATE -O1) # 修改这个选项进行多次测试
# Build benchmark in Release mode.
set(CMAKE_BUILD_TYPE "Release")
set(BENCHMARK_ENABLE_TESTING OFF)
add_subdirectory(benchmark)
main.cpp(在对应的 Compiler Explorer 链接中,可以改不同的编译选项尝试一下):
#include <array>
#include <benchmark/benchmark.h>
#include <functional>
struct Foo {
virtual void f() = 0;
};
struct Bar : public Foo {
int counter = 0;
void f() override { counter++; }
};
static Bar bar;
static void virtual_function(benchmark::State &state) {
Foo &foo = bar;
for (auto _ : state) {
foo.f();
benchmark::DoNotOptimize(foo);
}
}
static void std_function(benchmark::State &state) {
std::function<void()> f = []() { bar.counter++; };
for (auto _ : state) {
f();
benchmark::DoNotOptimize(f);
}
}
static void std_function_no_soo(benchmark::State &state) {
std::function<void()> f = [a = std::array<char, 256>{}]() { bar.counter++; };
for (auto _ : state) {
f();
benchmark::DoNotOptimize(f);
}
}
BENCHMARK(virtual_function);
BENCHMARK(std_function);
BENCHMARK(std_function_no_soo);
BENCHMARK_MAIN();
本地机器运行的详细结果(为了方便对比,我将四次运行的结果拼接在了一起):
Run on (12 X 2208.01 MHz CPU s)
CPU Caches:
L1 Data 32 KiB (x6)
L1 Instruction 32 KiB (x6)
L2 Unified 256 KiB (x6)
L3 Unified 9216 KiB (x1)
--------------------------------------------------------------
Benchmark Time CPU Iterations
--------------------------------------------------------------
-O0:STL 多层封装没被优化,导致 std_function 和 std_function_no_soo 更慢
virtual_function 2.76 ns 2.76 ns 255623909
std_function 22.1 ns 22.1 ns 31487803
std_function_no_soo 20.4 ns 20.4 ns 34398254
-O1:因为有小对象优化,std_function 访存少一次,因而更快;其他两个速度差不多
virtual_function 1.78 ns 1.78 ns 383829588
std_function 1.30 ns 1.30 ns 542120015
std_function_no_soo 1.79 ns 1.79 ns 387368468
-O2/-O3:虚函数被内联;std_function 的调用代码被全部消除;std_function_no_soo 仍是需要访存两次得到函数指针
virtual_function 1.38 ns 1.38 ns 489559445
std_function 1.26 ns 1.26 ns 534617229
std_function_no_soo 1.54 ns 1.54 ns 455909928
Tip
包含大的函子的 std::function
(std_function_no_soo)在 -O0
下比包含小函子的更快一点点,这可能违反逻辑,但实际上是标准库封装的问题。因为没有优化, std::function
的 operator()
代码是相同的,在里面调用函子的逻辑也是相同的(先调用 _M_get_pointer(std::_Any_data const&)
获取指针再 invoke),但是两者的 _M_get_pointer
略有不同,在这个函数里:获取成员的引用这一步是用一个函数实现的,而获取函子的地址时,std_function_no_soo 是直接将指针返回,而 std_function 为了防止 &
运算符被重载,还调用了 std::__address_of
方法,这就多了一次函数调用。