理论上,使用 STL 算法和 lambda 仿函数相比手工编写的循环所带来的所有开销都可以被优化掉,就像标准 STL 函数对象和绑定器所带来的开销一样。根据编译器的不同,这在实践中也可能成立。我们在 1.5 GHz Intel Pentium 4 上使用 GCC 3.0.4 编译器进行了两项测试。使用了优化标志 -03。
在第一个测试中,我们将 lambda 仿函数与显式编写的函数对象进行了比较。我们使用了这两种风格来定义一元函数,该函数将参数自身反复相乘。我们从恒等函数开始,一直到 x5。这些表达式在 std::transform
循环中被调用,从一个 std::vector<int>
中读取参数并将结果放入另一个中。向量的长度为 100 个元素。运行时间列于表 17.3, “测试 1”。我们可以观察到这两种方法之间没有显著差异。
在第二个测试中,我们再次使用 std::transform
对一个 100 个元素长的向量中的每个元素执行一个操作。这次向量的元素类型是 double
,我们从非常简单的算术表达式开始,然后转向更复杂的表达式。运行时间列于表 17.4, “测试 2”。在这里,我们还将经典的 STL 风格的匿名函数包含在测试中。我们不展示这些表达式,因为它们相当复杂。例如,表 17.4, “测试 2”中的最后一个表达式使用经典的 STL 工具编写,包含 7 次对 compose2
的调用,8 次对 bind1st
的调用,以及总共 14 次构造函数调用来创建 multiplies
、minus
和 plus
对象。在此测试中,BLL 表达式比相应的动手编写的函数对象稍慢(平均慢约 10%,所有情况均慢不到 14%)。对于最简单的表达式,性能损失使用经典的 STL 表达式更大,高达 27%。
测试表明,BLL 与 STL 函数对象相比,不会带来性能损失。使用合理的优化编译器,性能特征应该与使用经典 STL 相当。此外,对于简单的表达式,性能预计将接近显式编写的函数对象的性能。但请注意,评估 lambda 仿函数由一系列对内联声明的小函数的调用组成。如果编译器未能实际内联展开这些函数,性能可能会受到影响。如果发生这种情况,运行时间可能会增加一倍以上。尽管上述测试不包括此类表达式,但我们已经遇到过一些看似简单的表达式出现这种情况。
表 17.3. 测试 1
表达式 | lambda 表达式 | 手工编写的函数对象 |
---|---|---|
x | 240 | 230 |
x*x | 340 | 350 |
x*x*x | 770 | 760 |
x*x*x*x | 1180 | 1210 |
x*x*x*x*x | 1950 | 1910 |
表 17.4. 测试 2
compose2
、bind1st
等)和传统手工编写的函数对象类编写的算术表达式的 CPU 时间。使用 BLL 术语,a
和 b
是表达式中的绑定参数,而 x
是未绑定变量。所有变量类型均为 double
。运行时间以任意单位表示。表达式 | lambda 表达式 | 经典 STL 表达式 | 手工编写的函数对象 |
---|---|---|---|
ax | 330 | 370 | 290 |
-ax | 350 | 370 | 310 |
ax-(a+x) | 470 | 500 | 420 |
(ax-(a+x))(a+x) | 620 | 670 | 600 |
((ax) - (a+x))(bx - (b+x))(ax - (b+x))(bx - (a+x)) | 1660 | 1660 | 1460 |
对库早期版本的其他一些性能测试描述在[Jär00]。
BLL 大量使用模板,并执行相同的模板的多次递归实例化。这(至少)有三个影响:
虽然可以编写极其复杂的 lambda 表达式,但这可能不是一个好主意。编译这些表达式可能会在编译时消耗大量内存,并且编译速度很慢。
即使是最简单的 lambda 表达式产生的 lambda 仿函数的类型也很晦涩。通常程序员根本不需要处理 lambda 仿函数的类型,但在 lambda 表达式出错的情况下,编译器通常会输出涉及的 lambda 仿函数的类型。这可能导致错误消息非常长且难以理解,特别是如果编译器输出整个模板实例化链。
C++ 标准建议模板嵌套深度为 17,以帮助检测无限递归。复杂的 lambda 模板很容易超出此限制。大多数编译器允许更多的嵌套模板,但通常需要通过命令行参数显式增加此限制。
BLL 可与以下编译器配合使用,即这些编译器能够编译 BLL 附带的测试用例:
exception_test.cpp
导致内部编译器错误。)
以下列表描述了包含的测试文件以及每个文件涵盖的功能:
bind_tests_simple.cpp
:绑定不同元数和目标函数类型的表达式:函数指针、函数对象和成员函数。使用绑定表达式进行函数组合。
bind_tests_simple_function_references.cpp
:重复bind_tests_simple.cpp
中的所有测试,其中目标函数是函数指针,但使用函数引用代替。
bind_tests_advanced.cpp
:包含嵌套绑定表达式、unlambda
、protect
、const_parameters
和 break_const
的测试。将 lambda 仿函数作为实际参数传递给其他 lambda 仿函数的测试、柯里化以及使用 sig
模板指定函数对象的返回类型的测试。
operator_tests_simple.cpp
:使用为 lambda 表达式重载的所有运算符的测试,即一元和二元算术、按位、比较、逻辑、增量和减量、复合、赋值、下标、取地址、解引用和逗号运算符。测试移位运算符的流式性质,以及加减运算符的指针算术。
member_pointer_test.cpp
:成员指针运算符足够复杂,需要单独的测试文件。
control_structures.cpp
:循环和 if 构造的测试。
switch_construct.cpp
:包含支持的所有元数的 switch 语句的测试,包括和不包括 default 情况。
exception_test.cpp
:包含抛出异常和 try/catch 构造的测试,具有不同数量的 catch 块。
constructor_tests.cpp
:包含对 constructor
、destructor
、new_ptr
、delete_ptr
、new_array
和 delete_array
的测试。
cast_test.cpp
:四种 cast 表达式的测试,以及 typeid
和 sizeof
。
extending_return_type_traits.cpp
:测试为用户定义类型扩展返回类型推导系统。包含几个用户定义的运算符以及返回类型推导模板的相应特化。
is_instance_of_test.cpp
:包含用于内部使用的 traits 模板的测试,该模板可以检测给定类型是否是特定模板的实例。
bll_and_function.cpp
:包含将 boost::function
与 lambda 仿函数一起使用的测试。