一个很好的C可变长度数组示例[关闭]


9

这个问题在SO上引起了不小的反响,所以我决定在那里删除它,然后在这里尝试。如果您认为它也不适合此处,请至少对如何找到我所追求的示例提出建议...

您能举个例子吗,使用C99 VLA相对于当前的标准堆(使用C ++ RAII机制)提供了真正的优势?

我遵循的示例应:

  1. 与使用堆相比,可轻松实现(可能达到10%)的性能优势。
  2. 没有一个好的解决方法,它根本不需要整个数组。
  3. 实际受益于使用动态尺寸,而不是固定的最大尺寸。
  4. 在正常使用情况下,不太可能导致堆栈溢出。
  5. 足够强大,足以吸引需要性能的开发人员在C ++项目中包含C99源文件。

在上下文中添加了一些说明:我的意思是C99所指的VLA,但不包括在标准C ++中:int array[n]这里n是变量。在一个用例示例中,我胜过其他标准(C90,C ++ 11)提供的替代方案:

int array[MAXSIZE]; // C stack array with compile time constant size
int *array = calloc(n, sizeof int); // C heap array with manual free
int *array = new int[n]; // C++ heap array with manual delete
std::unique_ptr<int[]> array(new int[n]); // C++ heap array with RAII
std::vector<int> array(n); // STL container with preallocated size

一些想法:

  • 带有varargs的函数,自然会将项目数限制为合理,但没有任何有用的API级别上限。
  • 递归函数,不需要浪费堆栈
  • 许多小的分配和发行,堆开销会很糟糕。
  • 处理性能至关重要的多维数组(如任意大小的矩阵),小型函数有望内联很多。
  • 注释:并发算法,堆分配具有同步开销

维基百科有一个不符合我标准的示例,因为至少在没有上下文的情况下,使用堆的实际区别似乎无关紧要。这也是不理想的,因为如果没有更多的上下文,看来项目计数很可能会导致堆栈溢出。

注意:我特别关注示例代码,或者是从中受益的算法建议,让我自己实现示例。


1
有点投机(因为这是锤子在寻找钉子),但由于后者中的锁争用,所以在多线程环境中可能alloca()确实会胜过。但这是一个真正的难题,因为小阵列应仅使用固定大小,而大阵列可能仍需要堆。malloc()
chrisaycock

1
@chrisaycock是的,很多锤子在寻找钉子,但实际上是存在的锤子(无论是C99 VLA还是实际上不是任何标准的锤子alloca,我认为它们基本上是同一回事)。但是那个多线程的东西很好,包括它的编辑问题!
海德

VLA的一个缺点是没有检测分配失败的机制。如果没有足够的内存,则该行为是不确定的。(固定大小的数组和alloca()也是如此。)
Keith Thompson

@KeithThompson嗯,也不能保证malloc / new也会检测到分配失败,例如,请参见Linux malloc手册说明(linux.die.net/man/3/malloc)。
海德

@hyde:Linux的malloc行为是否符合C标准尚有争议。
基思·汤普森

Answers:


9

我只是破解了一个小程序,该程序会生成一组随机数,每次都会在相同的种子处重新启动,以确保其“公平”和“可比”。在进行过程中,它将计算出这些值的最小值和最大值。而当它已经产生的一组数字,它计算多少都高于平均水平的minmax

对于VERY小型阵列,VLA的over具有明显的好处std::vector<>

这不是一个真正的问题,但是我们可以轻松地想象一下,我们将从一个小文件中读取值,而不是使用随机数,并使用相同类型的代码进行其他更有意义的计数/最小/最大计算。

对于相关函数中非常小的“随机数数量”(x)值,该vla解决方案大获全胜。随着大小变大,“获胜”会变小,并且给定足够的大小,矢量解似乎更有效率-不必过多研究该变体,因为当我们在VLA中开始有成千上万个元素时,真的是他们的本意...

而且我敢肯定有人会告诉我,有一些方法可以用一堆模板编写所有这些代码,并且可以在不运行RDTSC和cout运行时运行更多代码的情况下完成此工作……但是我认为那不是真的重点。

运行此特定变体时,我在func1(VLA)和func2(std :: vector)之间得到大约10%的差异。

count = 9884
func1 time in clocks per iteration 7048685
count = 9884
func2 time in clocks per iteration 7661067
count = 9884
func3 time in clocks per iteration 8971878

编译为: g++ -O3 -Wall -Wextra -std=gnu++0x -o vla vla.cpp

这是代码:

#include <iostream>
#include <vector>
#include <cstdint>
#include <cstdlib>

using namespace std;

const int SIZE = 1000000;

uint64_t g_val[SIZE];


static __inline__ unsigned long long rdtsc(void)
{
    unsigned hi, lo;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}


int func1(int x)
{
    int v[x];

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v[i] = rand() % x;
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}

int func2(int x)
{
    vector<int> v;
    v.resize(x); 

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v[i] = rand() % x;
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}    

int func3(int x)
{
    vector<int> v;

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v.push_back(rand() % x);
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}    

void runbench(int (*f)(int), const char *name)
{
    srand(41711211);
    uint64_t long t = rdtsc();
    int count = 0;
    for(int i = 20; i < 200; i++)
    {
        count += f(i);
    }
    t = rdtsc() - t;
    cout << "count = " << count << endl;
    cout << name << " time in clocks per iteration " << dec << t << endl;
}

struct function
{
    int (*func)(int);
    const char *name;
};


#define FUNC(f) { f, #f }

function funcs[] = 
{
    FUNC(func1),
    FUNC(func2),
    FUNC(func3),
}; 


int main()
{
    for(size_t i = 0; i < sizeof(funcs)/sizeof(funcs[0]); i++)
    {
        runbench(funcs[i].func, funcs[i].name);
    }
}

哇,我的系统显示VLA版本比提升了30%std::vector
chrisaycock 2013年

1
好吧,尝试使用大约5-15而不是20-200的尺寸范围,您可能会获得1000%或更多的改善。[还取决于编译器选项-我将编辑上面的代码以在gcc上显示我的编译器选项]
Mats Petersson

我刚刚添加了一个func3,用于v.push_back(rand())代替,v[i] = rand();并消除了对的需要resize()。与使用相比,它花费的时间大约长10%resize()。[当然,在此过程中,我发现使用v[i]是函数花费时间的主要贡献者,对此我感到有些惊讶]。
Mats Petersson

1
@MikeBrown您知道std::vector使用VLA / 的实际实现alloca吗,还是仅仅是猜测?
海德

3
向量确实确实在内部使用数组,但据我所知,它无法使用VLA。我确实相信我的示例表明,VLA在某些(也许甚至很多)数据量很少的情况下很有用。即使向量使用了VLA,也需要在vector实现过程中付出额外的努力。
Mats Petersson

0

关于VLA与向量

您是否认为Vector可以利用VLA本身。没有VLA,向量必须指定数组的某些“比例”,例如10、100、10000进行存储,因此最终需要分配10000个项目的数组来容纳101个项目。使用VLA,如果将大小调整为200,则算法可能会假设您只需要200,并且可以分配200个项目数组。或者它可以分配一个缓冲区,例如n * 1.5。

无论如何,我会争辩说,如果您知道在运行时需要多少个项目,那么VLA的性能会更高(如Mats的基准测试所示)。他演示了一个简单的两遍迭代。想想一下蒙特卡洛模拟(其中重复采样随机样本)或图像处理(如Photoshop滤镜),其中对每个元素进行多次计算,并且很可能对每个元素进行的每次计算都涉及查看邻居。

从向量跳转到其内部数组的额外指针累加起来。

回答主要问题

但是,当您谈论使用动态分配的结构(如LinkedList)时,没有可比性。数组使用指针算术对其元素进行直接访问。使用链表,您必须遍历节点才能到达特定元素。因此,在这种情况下,VLA胜出了。

根据此答案,它在体系结构上是相关的,但是在某些情况下,由于堆栈在缓存中可用,因此堆栈上的内存访问会更快。如果有很多因素,这可以被否定(这可能是Mats在基准测试中看到的收益递减的原因)。但是,值得注意的是,高速缓存的大小正在显着增长,并且您可能会看到更多的相应增长。


我不确定我是否理解您对链接列表的引用,因此我在问题中添加了一部分,进一步说明了上下文并添加了我正在考虑的替代示例。
海德

为什么std::vector需要缩放数组?为什么只需要101个元素就需要10K元素的空间?此外,该问题从未提及链接列表,因此我不确定您从何处获得链接列表。最后,将C99中的VLA进行堆栈分配。它们是的标准形式alloca()。任何需要堆存储(在函数返回后仍然存在)或realloc()(数组会自行调整大小)的内容都将禁止VLA。
chrisaycock

@chrisaycock C ++出于某种原因缺少realloc()函数,假设内存是用new []分配的。这不是std :: vector必须使用标度的主要原因吗?

@Lundin C ++是否以十的幂来缩放向量?我给人的印象是,鉴于链接列表的引用,麦克·布朗对这个问题确实感到困惑。(他还作了一个较早的断言,暗示C99 VLA存在于堆中。)
chrisaycock 2013年

@海德我没意识到那是你在说的。我以为您的意思是其他基于堆的数据结构。有趣的是,您已经添加了此说明。我还没有足够的C ++怪才告诉您两者之间的区别。
迈克尔·布朗

0

使用VLA的原因主要是性能。忽略Wiki示例仅具有“无关”差异是错误的。我可以很容易地看到某些情况下,确切的代码可能会有很大的不同,例如,如果该函数在紧密循环中被调用,那么read_val在某些速度至关重要的系统上,IO函数会很快返回。

实际上,在大多数以这种方式使用VLA的地方,它们不会替换堆调用,而是替换如下内容:

float vals[256]; /* I hope we never get more! */

关于任何本地声明的事情是,它非常快。该行float vals[n]通常只需要几个处理器指令(可能只有一个)。它只是将值添加n到堆栈指针中。

另一方面,堆分配需要遍历数据结构以找到可用区域。即使在最幸运的情况下,时间也可能要长一个数量级。(即放在n堆栈上并进行调用的行为malloc可能是5-10条指令。)如果堆中有足够数量的数据,则可能会更糟。看到malloc一个实际程序中的速度慢100到1000倍的情况,这一点都不令我感到惊讶。

当然,匹配也会对性能产生影响,其free大小可能与malloc调用相似。

此外,还有内存碎片的问题。很多小的分配往往会使堆碎片化。碎片堆既浪费内存,又增加了分配内存所需的时间。


关于Wikipedia示例:它可能是一个好示例的一部分,但是如果没有上下文,并且没有更多代码,它并不能真正显示我的问题中列举的5件事。否则,我同意你的解释。尽管要记住一件事:使用VLA可能会花费访问局部变量的代价,因为在编译时不一定知道所有局部变量的偏移量,因此必须注意不要将一次性堆成本替换为每次迭代都有内循环惩罚。
海德

嗯...不确定你的意思。局部变量声明是单个操作,任何经过适当优化的编译器都会将分配拉出内部循环。访问局部变量没有特别的“成本”,当然也不是VLA会增加的成本。
2013年

具体示例:在编译时将不会知道的int vla[n]; if(test()) { struct LargeStruct s; int i; }堆栈偏移量s,并且编译器是否会将i内部范围之外的存储移至固定的堆栈偏移量也令人怀疑。因此需要额外的机器代码,因为间接寻址也可能耗尽寄存器,这在PC硬件上很重要。如果您想提供包含编译器程序集输出的示例代码,请询问一个单独的问题;)
hyde 2013年

编译器不必按代码中遇到的顺序进行分配,并且是否分配和不使用空间都没有关系。对于一款智能优化器将分配空间si进入功能时,之前test被称为或vla分配,作为拨款si没有任何副作用。(实际上,i甚至可能放置在寄存器中,这意味着根本没有“分配”。)编译器无法保证堆栈上分配的顺序,甚至不能保证使用堆栈。

(删除的评论由于愚蠢的错误而错了)
hyde 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.