繁体   English   中英

静态数组与动态数组的C / C ++性能

[英]C/C++ performance of static arrays vs dynamic arrays

当性能对应用程序至关重要时,应该考虑是否在堆栈上声明一个数组而不是堆? 请允许我概述为什么会出现这个问题。

由于C / C ++中的数组不是对象并且衰减为指针,因此编译器使用提供的索引来执行指针算法来访问元素。 我的理解是,当经过第一个维度时,此过程静态声明的数组不同,是动态声明的数组。

如果我要在堆栈上声明一个数组,如下所示;

  int array[2][3] = { 0, 1, 2, 3, 4, 5 }
  //In memory        { row1 } { row2 }

该数组将以行主格式存储在内存中,因为它存储在连续的内存块中。 这意味着当我尝试访问数组中的元素时,编译器必须执行一些加法和乘法才能确定正确的位置。

所以如果我要做以下事情

  int x = array[1][2]; // x = 5

然后编译器将使用以下公式:

i =行索引j =列索引n =单行的大小(此处n = 2)
array =指向第一个元素的指针

  *(array + (i*n) + j)
  *(array + (1*2) + 2)  

这意味着如果我循环遍历此数组以访问其每个元素,则通过索引对每个访问执行额外的乘法步骤。

现在,在堆上声明的数组中,范例是不同的,需要一个多阶段解决方案。 注意:我也可以在这里使用C ++ new运算符,但我相信数据的表示方式没有区别。

  int ** array;
  int rowSize = 2;
  // Create a 2 by 3 2d array on the heap
  array = malloc(2 * sizeof(int*));
  for (int i = 0; i < 2; i++) {
      array[i] = malloc(3 * sizeof(int));
  }

  // Populating the array
  int number = 0;
  for (int i = 0; i < 2; i++) {
      for (int j = 0l j < 3; j++) {
          array[i][j] = number++;
      }
  }

由于数组现在是动态的,因此其表示是一维数组的一维数组。 我会尝试绘制ascii图片......

              int *        int int int
int ** array-> [0]          0   1   2
               [1]          3   4   5

这意味着不再涉及乘法吗? 如果我要做以下事情

int x = array[1][1];

然后,这将对array [1]执行间接/指针算法以访问指向第二行的指针,然后再次执行此操作以访问第二个元素。 我说的是对的吗?

现在有一些背景,回到问题。 如果我正在为需要清晰性能的应用程序编写代码,比如渲染帧大约需要0.016秒的游戏,那么我应该三思而后行使用堆栈中的数组与堆相比? 现在我意识到使用malloc或new运算符需要一次性成本,但是在某个时刻(就像Big O分析一样)当数据集变大时,最好通过动态数组迭代来避免行主索引?

这些将适用于“普通”C(不是C ++)。

首先让我们清楚一些术语

“static”是C中的关键字,如果将变量应用于函数内声明的变量,它将极大地改变变量的分配/访问方式。

有3个位置(关于C),其中一个变量(包括数组)可能位于:

  • Stack:这些是没有static函数局部变量。
  • 数据部分:程序启动时为这些分配空间。 这些是任何全局变量(无论是否为static变量,关键字与可见性有关),以及任何声明为static函数局部变量。
  • 堆:由指针引用的动态分配的内存( malloc()free() )。 您只能通过指针访问此数据。

现在让我们看看如何访问一维数组

如果访问具有常量索引的数组(可能是#define d,但在普通C中不是const ),则可以由编译器计算此索引。 如果在“ 数据”部分中有一个真实数组,则无需任何间接访问它。 如果堆栈上有指针( Heap )或数组,则始终需要间接。 因此,具有此类访问权限的数据部分中的数组可能会快得多。 但这不是一个可以改变世界的非常有用的东西。

如果访问具有索引变量的数组,则它基本上总是衰减到指针,因为索引可能会更改(例如for循环中的增量)。 对于所有类型,生成的代码可能非常相似甚至相同。

带来更多尺寸

如果声明一个两维或更多维数组,并通过常量部分或完全访问它,智能编译器可能会如上所述优化这些常量。

如果您通过索引访问,请注意内存是线性的。 如果真实数组的后续维度不是2的倍数,则编译器将需要生成乘法。 例如在数组int arr[4][12]; 第二个维度是12.如果你现在以arr[i][j]访问它,其中ij是索引变量,线性存储器必须被索引为12 * i + j 因此编译器必须生成代码以在此处与常量相乘。 复杂性取决于常数与2的幂的“远”程度。这里得到的代码看起来有点像计算(i<<3) + (i<<2) + j来访问数组中的元素。

如果从指针构建二维“数组”,则维度的大小无关紧要,因为结构中存在引用指针。 这里如果你可以编写arr[i][j] ,这意味着你将它声明为例如int* arr[4] ,然后malloc()编辑了四个12 int的内存块。 请注意,您的四个指针(编译器现在可以用作基础)也会占用内存,如果它是真正的数组则不会占用内存。 还要注意,这里生成的代码将包含一个双重间接:首先,代码从arr加载i的指针,然后它将从该指针加载一个由jint

如果长度与2的幂“相差”(因此必须生成复杂的“乘以常数”代码来访问元素),那么使用指针可以生成更快的访问代码。

正如James Kanze在他的回答中提到的,在某些情况下,编译器可能能够优化真正的多维数组的访问。 对于由指针组成的数组,这种优化是不可能的,因为“数组”实际上不是这种情况下的线性内存块。

地方很重要

如果您正在开发通常的桌面/移动架构(Intel / ARM 32/64位处理器),那么地方也很重要。 这可能就是坐在缓存中。 如果您的变量由于某种原因已经在缓存中,则可以更快地访问它们。

在局部性方面, Stack总是赢家,因为Stack经常被使用,因此很可能总是位于缓存中。 所以小阵列最好放在那里。

使用真正的多维数组而不是从指针组成一个数组也可能有助于此,因为真正的数组总是一个线性的内存块,所以它通常可能需要更少的缓存块来加载。一个分散的指针组合(即如果单独使用malloc() ed chunks)相反可能需要更多的缓存块,并且可能会升级缓存行冲突,具体取决于块在堆上的物理结束方式。

至于哪种选择提供更好的性能,那么答案在很大程度上取决于您的具体情况。 了解一种方法是否更好或者它们大致相同的唯一方法是衡量应用程序的性能。

一些因素是:你经常这样做,数组/数据的实际大小,你的系统有多少内存,以及你的系统管理内存的程度。

如果您能够在两种选择之间做出选择,那么它必须意味着尺寸已经确定。 然后,您不需要您说明的多重分配方案。 您可以执行2D阵列的单个动态分配。 在C:

int (*array)[COLUMNS];
array = malloc(ROWS * sizeof(*array));

在C ++中:

std::vector<std::array<int, COLUMNS>> array(ROWS);

只要COLUMNS被固定,您就可以执行单个分配来获取2D阵列。 如果两者都没有被钉死,那么你无论如何都无法选择使用静态数组。

在C ++中实现二维数组的通常方法是使用std::vector<int>将它包装在一个类中,并具有计算索引的类访问器。 然而:

有关优化的任何问题都只能通过测量来回答,即使这样,它们仅对您正在使用的编译器有效,也可以在您进行测量的机器上进行。

如果你写:

int array[2][3] = { ... };

然后像:

for ( int i = 0; i != 2; ++ i ) {
    for ( int j = 0; j != 3; ++ j ) {
        //  do something with array[i][j]...
    }
}

很难想象一个编译器实际上不会生成以下内容:

for ( int* p = array, p != array + whatever; ++ p ) {
    //  do something with *p
}

这是最基本的优化之一,并且已经持续了至少30年。

如果按照建议动态分配,编译器将无法应用此优化。 甚至对于单个访问:矩阵具有较差的局部性,并且需要更多的存储器访问,因此可能性能较差。

如果您使用的是C ++,则通常会编写一个Matrix类,使用std::vector<int>作为内存,并使用乘法显式计算索引。 (改进的局部性可能会导致更好的性能,尽管有乘法。)这可能使编译器更难以进行上述优化,但如果这是一个问题,你总是可以提供专门的迭代器来处理这个一个特例。 您最终会获得更具可读性和更灵活的代码(例如,维度不必保持不变),几乎不会损失性能。

通常在内存消耗和速度之间存在折衷。 根据经验,我亲眼目睹了在堆栈上创建数组比在堆上分配更快。 随着阵列大小的增加,这变得更加明显。

您始终可以减少内存消耗。 例如,您可以使用short或char而不是int等。

随着阵列大小的增加,特别是使用realloc,可能会有更多的页面替换(向上和向下)来维护项目的连续位置。

你还应该考虑堆栈中可以存储的东西的大小有一个下限,对于堆这个限制更高,但正如我所说的那样,性能成本。

与堆相比,Stalk内存分配可以更快地访问数据。 如果没有它,CPU会在缓存中查找地址,如果它没有在缓存中找到地址,那么它将在主存储器中查找。 Stalk是缓存后的首选位置。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM