我一直在做一些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 ++解决方案,完全相同,但作为Solution类的成员函数在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 ++版本完全相同,但作为Solution类的成员函数,运行时间为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&gt;( - 1U&gt;&gt; 1)|| rev&lt;(1&lt;&lt; 31)){ 同 if(rev&gt; INT_MAX || rev&lt; INT_MIN){
现在,我认为必须#include<limits.h>
可能与此有关,但似乎有点极端,这个简单的改变将执行时间从6毫秒减少到19毫秒。
答案 0 :(得分:42)
最近我在C ++中使用2d数组时看到了vector<vector<int>>
很多建议,而且我已经向人们指出为什么这不是一个好主意。知道何时将临时代码拼凑在一起是一个方便的技巧,但是(几乎)从来没有任何理由将其用于实际代码。 right thing to do是使用包装连续内存块的类。
所以我的第一反应可能是指出这是造成这种差异的可能来源。但是,您还在C版本中使用int**
,这通常表示与vector<vector<int>>
完全相同的问题。
所以我决定只比较两种解决方案。
http://coliru.stacked-crooked.com/a/fa8441cc5baa0391
6468424
6588511
&#39; C版本所花费的时间&#39; vs&#39; C ++版本&#39;以纳秒为单位。
我的结果不会显示您描述的差异。然后我突然想到检查人们在基准测试时犯的一个常见错误
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显然没有产生苹果与苹果的比较。
答案 1 :(得分:-4)
您正在使用C ++代码段中的向量向量。向量是C ++中的序列容器,类似于可以改变大小的数组。如果您使用静态分配的数组,而不是vector<vector<int>>
,那会更好。您可以使用自己的Array类以及operator []重载,但是当您添加的元素多于其原始大小时,vector会动态调整大小,因此vector会产生更多开销。在C ++中,如果将其与C进行比较,则使用按引用调用来进一步减少时间。如果写得好,C ++应该运行得更快。