C++中对应C风格数组的语法

11

我听到很多人说C++ 在所有方面都与C一样快或者更快,而且更加整洁和优雅。

虽然我不否认 C++非常优雅,速度也很快,但是对于关键的内存访问或处理器密集型应用程序,我没有找到替代品。

问题:在性能方面,C++中是否有与C风格数组等效的东西?

下面的例子是刻意构造的,但我对真实问题的解决方案感兴趣:我开发图像处理应用程序,那里的像素处理量非常大。

double t;

// C++ 
std::vector<int> v;
v.resize(1000000,1);
int i, j, count = 0, size = v.size();

t = (double)getTickCount();

for(j=0;j<1000;j++)
{
    count = 0;
    for(i=0;i<size;i++)
         count += v[i];     
}

t = ((double)getTickCount() - t)/getTickFrequency();
std::cout << "(C++) For loop time [s]: " << t/1.0 << std::endl;
std::cout << count << std::endl;

// C-style

#define ARR_SIZE 1000000

int* arr = (int*)malloc( ARR_SIZE * sizeof(int) );

int ci, cj, ccount = 0, csize = ARR_SIZE;

for(ci=0;ci<csize;ci++)
    arr[ci] = 1;

t = (double)getTickCount();

for(cj=0;cj<1000;cj++)
{
    ccount = 0;
    for(ci=0;ci<csize;ci++)
        ccount += arr[ci];      
}

free(arr);

t = ((double)getTickCount() - t)/getTickFrequency();
std::cout << "(C) For loop time [s]: " << t/1.0 << std::endl;
std::cout << ccount << std::endl;

以下是结果:

(C++) For loop time [s]: 0.329069

(C) For loop time [s]: 0.229961

注意:getTickCount()来自第三方库。如果你想进行测试,只需替换为你喜欢的时钟测量即可。
更新:
我正在使用VS 2010,发布模式,其他所有默认设置。

8
我怀疑你没有进行完全优化的C++编译。 - Luchian Grigore
5
这段代码中没有指针数组... - unwind
2
我刚刚测试了这个(稍微修改了代码,在一台计算机上,使用一个特定的编译器)。没有优化的情况下,“C++风格”的比“C风格”的慢约三分之一。经过优化,“C++风格”的始终比“C风格”的略快(两者都比未优化的要快得多)。 - Mankarse
3
在我的机器上,C和C++版本都需要0.215秒的时间(使用GCC和G++在x86-64机器上)。由于某种原因,C++ 32位速度更快(分别为0.534秒和0.605秒)。 - David Schwartz
2
当使用g++ -O3编译时,我得到了两个版本的相同运行时间。 - halex
显示剩余5条评论
6个回答

13
简单回答:你的基准测试存在缺陷。
更长的回答是:你需要打开完整优化以获得C++性能优势。然而,你的基准测试仍然存在缺陷。
以下是一些观察结果:
1. 如果你打开完整优化,很大一部分for循环将被移除,这使你的基准测试毫无意义。 2. std::vector有动态重新分配的开销,请尝试使用std::array。具体来说,Microsoft的STL默认有checked iterator。 3. 你没有任何障碍来防止C/C++代码/基准测试代码之间的交叉重排序。 4. (与主题不太相关)cout << ccount是区域设置感知的,printf则不是;std::endl刷新输出,printf("\n")则不会。
“传统”的用于展示C++优势的代码是C的qsort()和C++的std::sort()。这是内联代码发挥作用的地方。
如果你想要一些“现实生活”应用的例子,可以搜索一些光线追踪或矩阵乘法的内容。选择一个可以进行自动向量化的编译器。
更新: 使用LLVM在线演示,我们可以看到整个循环被重新排序了。基准测试代码被移到了开头,并在第一个循环中跳转到循环结束点,以便更好地进行分支预测。
(这是C ++代码)
######### jump to the loop end
    jg  .LBB0_11
.LBB0_3:                                # %..split_crit_edge
.Ltmp2:
# print the benchmark result
    movl    $0, 12(%esp)
    movl    $25, 8(%esp)
    movl    $.L.str, 4(%esp)
    movl    std::cout, (%esp)
    calll   std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
.Ltmp3:
# BB#4:                                 # %_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc.exit
.Ltmp4:
    movl    std::cout, (%esp)
    calll   std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<double>(double)
.Ltmp5:
# BB#5:                                 # %_ZNSolsEd.exit
    movl    %eax, %ecx
    movl    %ecx, 28(%esp)          # 4-byte Spill
    movl    (%ecx), %eax
    movl    -24(%eax), %eax
    movl    240(%eax,%ecx), %ebp
    testl   %ebp, %ebp
    jne .LBB0_7
# BB#6:
.Ltmp52:
    calll   std::__throw_bad_cast()
.Ltmp53:
.LBB0_7:                                # %.noexc41
    cmpb    $0, 28(%ebp)
    je  .LBB0_15
# BB#8:
    movb    39(%ebp), %al
    jmp .LBB0_21
    .align  16, 0x90
.LBB0_9:                                #   Parent Loop BB0_11 Depth=1
                                        # =>  This Inner Loop Header: Depth=2
    addl    (%edi,%edx,4), %ebx
    addl    $1, %edx
    adcl    $0, %esi
    cmpl    %ecx, %edx
    jne .LBB0_9
# BB#10:                                #   in Loop: Header=BB0_11 Depth=1
    incl    %eax
    cmpl    $1000, %eax             # imm = 0x3E8
######### jump back to the print benchmark code
    je  .LBB0_3

我的测试代码:
std::vector<int> v;
v.resize(1000000,1);
int i, j, count = 0, size = v.size();

for(j=0;j<1000;j++)
{
    count = 0;
    for(i=0;i<size;i++)
         count += v[i];     
}

std::cout << "(C++) For loop time [s]: " << t/1.0 << std::endl;
std::cout << count << std::endl;

1
嗯,我在MSVC中使用了默认的Release模式。我试图从循环中排除所有的分配/输出等操作。std::cout被策略性地排除在基准测试之外。这似乎更像是编译器的问题,因为g++对两种代码都给出了相同的时间。 - Sam
1
你是说他的代码“写错了”吗?他的代码是有效的基准,应该为两种语言生成相同的指令。如果不是这样,那么编译器或STL就存在问题。 - Lubo Antonov
2
(2)不是真的。在时间检查之前,数组已经被预先调整大小,因此不会进行重新分配。至于(1),我会(部分地)展开循环(以减少与循环相关的内容),并使计数易挥发以排除不希望的优化。 - user396672
1
@user396672,将ccount设为易失性变量也会阻止向量化。我们需要停止循环倒置/消除,而不是向量化。 - J-16 SDiZ
2
@J-16SDiZ 在2010年,_SECURE_SCL的默认值为0。http://msdn.microsoft.com/en-us/library/aa985896.aspx - hansmaad
显示剩余4条评论

12

问题:在性能方面,C++中是否有与C语言数组相当的东西?

回答:写C++代码!了解你的语言、了解你的标准库并使用它。标准算法是正确、可读且快速的(它们知道如何在当前编译器上实现为快速)。

void testC()
{
    // unchanged
}

void testCpp()
{
    // unchanged initialization

    for(j=0;j<1000;j++)
    {
        // how a C++ programmer accumulates:
        count = std::accumulate(begin(v), end(v), 0);    
    }

    // unchanged output
}

int main()
{
    testC();
    testCpp();
}

输出:

(C) For loop time [ms]: 434.373
1000000
(C++) For loop time [ms]: 419.79
1000000

使用 g++ -O3 -std=c++0x 版本4.6.3在Ubuntu上编译。

对于您的代码,我的输出与您的类似。user1202136给出了一个有关差异的很好的答案...


1
你能发一下你的时间分析代码吗?我使用std::accumulate时得到了非常奇怪的结果。 - juanchopanza
@juanchopanza 我使用了 <sys/time.h> 中的 gettimeofday - hansmaad
1
我认为这就是答案...尽管在我的测试平台(MSVC)上比其他任何东西都要慢。但我不能因为似乎是微软的问题而责怪C++。 - Sam

8

这似乎是编译器问题。对于C数组,编译器能够检测到模式,使用自动向量化并发出SSE指令。但对于向量,则似乎缺乏必要的智能。

如果我强制编译器不使用SSE,则结果非常相似(使用g++ -mno-mmx -mno-sse -msoft-float -O3进行测试):

(C++) For loop time [us]: 604610
1000000
(C) For loop time [us]: 601493
1000000

这是生成此输出的代码。它基本上是您提出问题的代码,但没有任何浮点数。
#include <iostream>
#include <vector>
#include <sys/time.h>

using namespace std;

long getTickCount()
{
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec * 1000000 + tv.tv_usec;
}

int main() {
long t;

// C++ 
std::vector<int> v;
v.resize(1000000,1);
int i, j, count = 0, size = v.size();

t = getTickCount();

for(j=0;j<1000;j++)
{
    count = 0;
    for(i=0;i<size;i++)
         count += v[i];     
}

t = getTickCount() - t;
std::cout << "(C++) For loop time [us]: " << t << std::endl;
std::cout << count << std::endl;

// C-style

#define ARR_SIZE 1000000

int* arr = new int[ARR_SIZE];

int ci, cj, ccount = 0, csize = ARR_SIZE;

for(ci=0;ci<csize;ci++)
    arr[ci] = 1;

t = getTickCount();

for(cj=0;cj<1000;cj++)
{
    ccount = 0;
    for(ci=0;ci<csize;ci++)
        ccount += arr[ci];      
}

delete arr;

t = getTickCount() - t;
std::cout << "(C) For loop time [us]: " << t << std::endl;
std::cout << ccount << std::endl;
}

这与SSE或MMX无关,只与开启优化有关。另外,gettimeofday()不适合用于基准测试,因为它还记录了进程外部花费的时间。如果您在Unix上,请使用getrusage()。 - Nordic Mainframe
6
@LutherBlissett 对不起,但 SSE 或 MMX 问题所在。我检查了汇编输出。优化已经通过 -O3 打开。我使用了 gettimeofday,因为听说 getrusage 分辨率较差。为了减少基准测试误差,我确保系统大部分时间处于空闲状态,并多次运行基准测试,结果相似。 - user1202136

4

动态大小数组的C++等效类型是std::vector。固定大小数组的C++等效类型为std::array或C++11之前的std::tr1::array

如果您的向量代码没有重新分配大小,则很难看出它与使用动态分配的C数组相比存在明显的速度差异,只要您启用了一些优化进行编译。

注意:在x86上的gcc 4.4.3上编译,编译器选项如下:

g++-Wall -Wextra -pedantic-errors -O2 -std=c++0x

结果重复接近于:

(C++) For loop time [us]: 507888

1000000

(C) For loop time [us]: 496659

1000000

因此,在少数试验之后,std::vector变体似乎慢了约2%。我认为这种性能是兼容的。


1
性能分析在向量初始化后开始,因此那不可能是问题... - Luchian Grigore
1
从零开始调整大小可能与分配没有什么不同。 - David Schwartz
1
@DavidSchwartz 可能,但这似乎是不必要的,并且表明您对std::vector不太了解。 - juanchopanza
2%的数字是误导性的,因为它很容易在误差范围内。两个代码之间应该没有统计学上显著的差异。 - Konrad Rudolph
@KonradRudolph 我不想过多猜测什么是“应该的”,但我承认我只运行了程序大约10次。在这个小实验中,偏移量保持在2%左右。但就我而言,性能是相同的。我觉得令人困惑的是使用“std::accumulate”的结果。 - juanchopanza
显示剩余7条评论

0

你指出的事实是访问对象总是会带来一些开销,因此访问 vector 不会比访问一个好老的数组更快。

但即使使用数组是“C风格”,它仍然是C++,所以不会有问题。

然后,正如 @juanchopanza 所说,在 C++11 中有 std::array,它可能比 std::vector 更有效率,但专门用于固定大小的数组。


5
有std::array在C++11中,比std::vector更高效。能否支持这个说法? - Luchian Grigore
1
如果您对向量进行更改大小的操作,例如循环push_back而不使用resize,则数组会更有效率。 - David Schwartz
2
数组(array)在某些情况下可能更有效率,因为 vector 包含一个额外的间接层(它包含指向数据的指针,而 array 包含数据本身)。是的,编译器会将读取数据指针的操作提升到循环外部,但这样做会消耗一个寄存器(用于存储指针),而访问 std::array 可以直接基于堆栈指针。使用一个寄存器只是一个小的性能问题,但有时可能会产生影响。出于同样的原因,使用更多堆栈会影响缓存,所以 array 在某些情况下也可能更慢。“可能”是一个容易达成的目标 :-) - Steve Jessop
如果没有其他情况,我会期望std::array通常更快,因为栈分配通常优于堆分配,并且std::array具有一个较少的指针间接引用,正如你所说。我会期望通过指向动态分配的std::array的指针访问与访问std::vector相同。至少在我的许多领域中,堆分配/释放通常是性能最差的因素。 - David Stone

0

通常编译器会进行所有的优化...你只需要选择一个好的编译器


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接