【发布时间】:2012-01-19 20:13:38
【问题描述】:
我在网上看过几次,有人提到 C++ 使用模板可以更快。
有人可以解释一下,包括在低层次上解释为什么会这样吗?我一直认为这样一个“不错”的功能会像大多数有用的概念一样产生开销。
从超低延迟的角度来看,我对此非常感兴趣!
【问题讨论】:
-
templates 是编译时特性。它可能会减少运行时间,但会增加编译时间。如果不加注意地使用,在某些情况下可能会导致代码膨胀。
我在网上看过几次,有人提到 C++ 使用模板可以更快。
有人可以解释一下,包括在低层次上解释为什么会这样吗?我一直认为这样一个“不错”的功能会像大多数有用的概念一样产生开销。
从超低延迟的角度来看,我对此非常感兴趣!
【问题讨论】:
templates 是编译时特性。它可能会减少运行时间,但会增加编译时间。如果不加注意地使用,在某些情况下可能会导致代码膨胀。
一个常见的例子是排序。
在 C 中,qsort 采用指向比较函数的指针。一般来说,qsort 代码会有一份,没有内联。它将通过指向比较例程的指针进行调用——这当然也不是内联的。
在 C++ 中,std::sort 是一个模板,它可以将函子对象作为比较器。对于用作比较器的每种不同类型,都有一个不同的 std::sort 副本。假设您使用具有重载 operator() 的仿函数类,那么对比较器的调用可以轻松内联到 std::sort 的此副本中。
因此,模板为您提供了更多内联,因为sort 代码的副本更多,每个副本都可以内联不同的比较器。内联是一个很好的优化,并且排序例程会进行大量比较,因此您通常可以测量 std::sort 的运行速度比同等的 qsort 运行得更快。这样做的代价是代码更大的可能性——如果你的程序使用很多不同的比较器,那么你会得到很多不同的排序例程副本,每个副本都包含不同的比较器。
原则上,C 实现没有理由不能将qsort 内联到它被调用的地方。然后,如果使用函数的名称调用它,优化器理论上可以观察到在使用它时,函数指针必须仍然指向同一个函数。然后它可以内联对函数的调用,结果将类似于std::sort 的结果。但在实践中,编译器往往不会采取第一步,内联qsort。那是因为 (a) 它很大,并且 (b) 它位于不同的翻译单元中,通常编译到您的程序链接的某个库中,并且 (c) 这样做,您将拥有 @ 的内联副本987654333@ 每次调用它,而不仅仅是每个不同比较器的副本。所以它会比 C++ 更臃肿,除非实现也能找到一种方法来在 qsort 在不同位置使用相同比较器调用的情况下共用代码。
因此,像 C 中的 qsort 这样的通用函数由于通过函数指针或其他间接调用 [*] 往往会产生一些开销。 C++ 中的模板是保持源代码通用的常用方法,但要确保它编译为一个特殊用途的函数(或几个这样的函数)。希望专用代码更快。
值得注意的是,模板绝不只是关于性能。在某些方面,std::sort 本身比qsort 更通用。例如qsort 只对数组进行排序,而std::sort 可以对任何提供随机访问迭代器的东西进行排序。例如,它可以对deque 进行排序,其背后是几个分开分配的不相交数组。因此,模板的使用不一定提供任何性能优势,它可能是出于其他原因。模板确实会影响性能。
[*] 另一个排序示例 - qsort 接受一个整数参数,表示数组的每个元素有多大,因此当它移动元素时,它必须调用 memcpy 或与此变量的值类似的值。 std::sort 在编译时知道元素的确切类型,因此知道确切的大小。它可以内联一个复制构造函数调用,进而可以转换为复制该字节数的指令。与内联比较器一样,通常可以精确地复制 4(或 8,或 16,或其他)字节,而不是通过调用复制可变字节数的例程,将值 4(或 8 , 或 16, 或其他)。和以前一样,如果您使用大小的文字值调用 qsort,并且对 qsort 的调用是内联的,那么编译器可以在 C 中执行完全相同的优化。但实际上您看不到。
【讨论】:
“更快”取决于您将其与什么进行比较。
模板由编译器完全评估,因此它们在运行时的开销为零。调用Foo<int>() 与调用FooInt() 一样有效。
因此,与依赖于在运行时完成更多工作(例如通过调用虚函数)的方法相比,模板确实可以更快。与专门为该场景编写的手写代码相比,差异为零。
因此,模板的好处不在于它们比其他方式“更快”,而是它们与手写代码“一样快”,同时还具有通用性和可重用性。
【讨论】:
另一个使用模板提高运行时性能的显着示例是Blitz++ 数字库。它率先使用所谓的expression templates,使用编译时逻辑将涉及大型向量和矩阵的算术表达式转换为更容易编译为高效机器代码的等价表达式。例如,给定以下伪代码:
vector<1000> a = foo(), b = bar(), c = baz(), result;
result = a + b + c;
一种简单的方法是将a 和b 的每个元素相加,将结果存储在临时向量中,然后对c 执行相同操作,最后将结果复制到result。使用表达式模板魔术,生成的代码将等效于:
for(int i = 0; i < 1000; ++i) {
result[i] = a[i] + b[i] + c[i];
}
这快得多,更好地利用缓存局部性并避免沿途不必要的临时性。它还避免了别名问题,其中编译器无法证明两个指针指向不同的内存区域,从而迫使它产生非最佳代码。表达式模板现在通常用于高性能数值,以及其他不涉及性能的用途,例如 Boost.Spirit 解析库。
【讨论】:
我不确定您是否在谈论 C++ 模板元编程:在编译期间进行一些计算,以便您在运行期间几乎立即获得结果。 如果是这样,这里有一个例子。
通过使用模板元编程和模板特化来提供递归的结束条件,程序中使用的阶乘,忽略任何未使用的阶乘,可以在编译时计算此代码
template <int N>
struct Factorial
{
enum { value = N * Factorial<N - 1>::value };
};
template <>
struct Factorial<0>
{
enum { value = 1 };
};
// Factorial<4>::value == 24
// Factorial<0>::value == 1
void foo()
{
int x = Factorial<4>::value; // == 24
int y = Factorial<0>::value; // == 1
}
这里还有一点要阅读 http://en.wikipedia.org/wiki/Template_metaprogramming
【讨论】:
matrix<float>,它的计算速度比包含对象引用的矩阵要快得多,为了实际使用二元运算符,必须向下转换。
他们很可能在谈论模板元编程,这是编译速度与运行时速度之间的权衡。基本思想是您可以编写一个将在 C++ 编译器中执行的程序。例如(从维基百科窃取):
template <int N>
struct Factorial
{
enum { value = N * Factorial<N - 1>::value };
};
template <>
struct Factorial<0>
{
enum { value = 1 };
};
// Factorial<4>::value == 24
// Factorial<0>::value == 1
void foo()
{
int x = Factorial<4>::value; // == 24
int y = Factorial<0>::value; // == 1
}
因此,要计算Factorial<4>::value,编译器需要“展开”模板并计算Factorial<3>::value 等等。这一切都是在编译时完成的,这显然会增加编译时间,但会在运行时有效地将其替换为常量值。
【讨论】:
模板被认为更快的原因是它们对编译器可见。
因此,虽然普通的 C 排序算法看起来像这样:
void qsort ( void * base, size_t num, size_t size,
int ( * comparator ) ( const void *, const void * ) );
接受一个函数指针来进行比较,从而在每次比较时调用一个函数,C++ 版本将如下所示:
template <class RandomAccessIterator, class StrictWeakOrdering>
void sort(RandomAccessIterator first, RandomAccessIterator last,
StrictWeakOrdering comp);
因此comp 是一个模板参数,如果它是一个定义了operator() 的类,编译器可以将函数的实现内联到循环中并避免许多函数调用。
我不认为模板元编程更快,因为这是大多数代码库中很少使用的功能。
【讨论】: