为啥我的 C++ 代码比 LeetCode 上的 C 代码慢三倍? [关闭]
Posted
技术标签:
【中文标题】为啥我的 C++ 代码比 LeetCode 上的 C 代码慢三倍? [关闭]【英文标题】:Why is my C++ code three times slower than the C equivalent on LeetCode? [closed]为什么我的 C++ 代码比 LeetCode 上的 C 代码慢三倍? [关闭] 【发布时间】:2015-04-08 18:08:15 【问题描述】:我一直在做一些LeetCode problems,我注意到 C 解决方案比 C++ 中完全相同的解决方案快几倍。例如:
更新了几个更简单的例子:
给定一个排序数组和一个目标值,如果找到目标则返回索引。如果不是,则返回按顺序插入的索引。您可以假设数组中没有重复项。 (Link to question on LeetCode)
我的 C 语言解决方案在 3 毫秒内运行:
int searchInsert(int A[], int n, int target)
int left = 0;
int right = n;
int mid = 0;
while (left<right)
mid = (left + right) / 2;
if (A[mid]<target)
left = mid + 1;
else if (A[mid]>target)
right = mid;
else
return mid;
return left;
我的其他 C++ 解决方案,完全相同,但作为解决方案类的成员函数在 13 毫秒内运行:
class Solution
public:
int searchInsert(int A[], int n, int target)
int left = 0;
int right = n;
int mid = 0;
while (left<right)
mid = (left + right) / 2;
if (A[mid]<target)
left = mid + 1;
else if (A[mid]>target)
right = mid;
else
return mid;
return left;
;
更简单的例子:
反转整数的数字。如果结果将溢出,则返回 0。 (Link to question on LeetCode)
C 版本在 6 毫秒内运行:
int reverse(int x)
long rev = x % 10;
x /= 10;
while (x != 0)
rev *= 10L;
rev += x % 10;
x /= 10;
if (rev>(-1U >> 1) || rev < (1 << 31))
return 0;
return rev;
和 C++ 版本完全一样,只是作为解决方案类的成员函数,运行时间为 19 毫秒:
class Solution
public:
int reverse(int x)
long rev = x % 10;
x /= 10;
while (x != 0)
rev *= 10L;
rev += x % 10;
x /= 10;
if (rev>(-1U >> 1) || rev < (1 << 31))
return 0;
return rev;
;
我看到如果 LeetCode 测试系统没有在启用优化的情况下编译代码,那么在原始示例中使用向量的向量作为 2D 数组会有相当大的开销。但是上面更简单的例子不应该遇到这个问题,因为数据结构非常原始,尤其是在第二种情况下,你所拥有的只是长整数或整数算术。这仍然慢了三倍。
我开始认为 LeetCode 通常进行基准测试的方式可能会发生一些奇怪的事情,因为即使在整数反转问题的 C 版本中,仅替换行就会导致运行时间大幅增加 if (rev>(-1U >> 1) || rev INT_MAX || rev
现在,我想#include<limits.h>
可能与此有关,但这个简单的更改将执行时间从 6 毫秒提高到 19 毫秒似乎有点极端。
【问题讨论】:
因为您使用的是向量的向量,这意味着您的每一列(或行)都是一个单独的分配,可能位于也可能不在缓存友好的位置。 这些时间是单次运行还是多次运行的平均值? 你优化了吗?一些 C++ 编译器在向量关闭时启用范围检查,例如在调试版本中。 您使用什么编译器选项?为什么将 C++ 解决方案包装在一个类中,您是否在计时器中包含了类初始化?说到这里,你是如何安排执行时间的? 分配方案不同吗? C 中的单个块与 C++ 中的多个块(向量)。无论如何,您应该更喜欢单个内存块(没有向量的向量)。 【参考方案1】:最近,我经常看到vector<vector<int>>
建议在 C++ 中执行二维数组,并且我一直在向人们指出为什么这真的不是一个好主意。知道何时将临时代码拼凑在一起是一个方便的技巧,但是(几乎)从来没有任何理由将它用于真正的代码。 right thing to do 是使用一个包装连续内存块的类。
所以我的第一反应可能是指出这是差异的可能来源。但是,您还在 C 版本中使用了int**
,这通常表示与vector<vector<int>>
完全相同的问题。
所以我决定只比较这两种解决方案。
http://coliru.stacked-crooked.com/a/fa8441cc5baa0391
6468424 6588511
这是“C 版本”与“C++ 版本”所用的时间(以纳秒为单位)。
我的结果与您描述的差异不符。然后我想到检查人们在进行基准测试时常犯的错误
http://coliru.stacked-crooked.com/a/e57d791876b9252b
18386695 42400612
请注意,第一个示例中的 -O3 标志已变为 -O0,这会禁用优化。
结论:您可能在比较未优化的可执行文件。
C++ 支持构建不需要开销的丰富抽象,但消除开销确实需要对代码的“可调试性”造成严重破坏的某些代码转换。
这意味着调试构建避免了这些转换,因此 C++ 调试构建通常比 C 样式代码的调试构建慢,因为 C 样式代码不使用太多抽象。在计时时,例如使用函数调用代替简单存储指令的机器代码,看到上述 130% 的减速一点也不奇怪。
有些代码确实需要优化,以便即使在调试时也能获得合理的性能,因此编译器通常会提供一种模式来应用一些不会给调试器带来太多麻烦的优化。 Clang 和 gcc 为此使用了-O1
,您可以看到,即使是这种级别的优化,也基本上消除了该程序中 C 风格代码和更多 C++ 风格代码之间的差距:
http://coliru.stacked-crooked.com/a/13967ebcfcfa4073
8389992 8196935
更新:
在后面的示例中,优化不应该产生影响,因为 C++ 没有使用 C 版本所做的任何抽象。我猜想对此的解释是这些示例是使用不同的编译器或其他一些不同的编译器选项编译的。在不知道编译是如何完成的情况下,我会说比较这些运行时数字是没有意义的; LeetCode 显然没有进行苹果与苹果的比较。
【讨论】:
我经常看到std::vector<T>
被优化为“原始”数组,但是,正如您所说的,您需要启用优化。使用 C,您已经在使用这种数组,因此未优化的代码会更快。
@bames53 实际上,vector<vector<T>>
并不总是比自定义结构更糟糕。这在很大程度上取决于应用程序。 (例如,添加另一列对于结构来说是一件非常昂贵的事情,但对于嵌套向量来说却不是)。与往常一样,应该分析实际用例。
附带说明,gcc 使用 -Og
进行调试器友好的优化。
@black:你见过vector
优化成一个原始数组吗?这是不可能的,除非优化器能够将堆分配转换为基于堆栈的分配,这远远超出了我所见过的任何编译器的优化能力。
指向堆分配的指针通常被称为原始数组,因为数组和指针语法在 C 中是可以互换的。从来没有听说过这个短语专门表示堆栈数组。【参考方案2】:
您在 C++ 代码 sn-p 中使用向量的向量。向量是 C++ 中的序列容器,就像可以改变大小的数组一样。如果您使用静态分配的数组,而不是vector<vector<int>>
,那会更好。您也可以使用自己的 Array 类以及重载运算符 [],但 vector 有更多开销,因为当您添加比其原始大小更多的元素时,它会动态调整大小。在 C++ 中,如果与 C 相比,您可以使用引用调用来进一步减少时间。如果写得好,C++ 应该会运行得更快。
【讨论】:
我不明白这是如何回答这个问题的。 C++ 代码是否改变了向量的大小?当然不是,那为什么要增加运行时间呢?该向量已经通过引用传递。虽然你已经做出了真实的陈述,但我不太明白它们与手头的问题有什么关系。 拥有这种简单的数据结构,C++ 绝对没有理由比 C 更快(或更慢)。 添加到@DieterLücking:C++ 的原始“编译器”输出只是 C 代码。它所做的只是将(当时的)“C++”转换为 C 代码,然后提供给 C 编译器。没有理由 C++ 会变慢。它的原始名称(我认为)是“C with classes”。 @RobKennedy 你知道为什么这个问题被否决了吗?罗宾逊的评论和 bames53 的回答说的基本相同。 我不知道你为什么要问我,@Nick。 Robinson 的评论表明问题在于缓存局部性。 Bames 的回答说问题在于优化。这些不是一回事。以上是关于为啥我的 C++ 代码比 LeetCode 上的 C 代码慢三倍? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章