更喜欢堆而不是堆栈?
Prefer heap over stack?
我最近投身于图形编程,我注意到许多图形引擎(即 Ogre)和总体上许多编码人员更喜欢动态初始化 class 实例。这是来自 Ogre Basic Tutorial 1
的示例
//...
Ogre::Entity* ogreHead = mSceneMgr->createEntity("Head", "ogrehead.mesh");
Ogre::SceneNode* headNode = mSceneMgr->getRootSceneNode()->createChildSceneNode("HeadNode");
//...
ogreHead
和 headNode
数据成员和方法被称为 ogreHead->blabla
.
为什么要乱用对象指针而不是普通对象?
顺便说一句,我还在某处读到堆内存分配比堆栈内存分配慢得多。
栈的范围是有限的:它只存在于一个函数中。现在,现代用户界面程序通常是事件驱动的,这意味着调用您的函数来处理事件,然后该函数必须 return 才能使程序继续 运行。因此,如果您的事件处理程序函数希望创建一个对象,该对象将在函数 returned 后继续存在,显然,该对象不能分配到该函数的堆栈上,因为它很快就会不复存在作为函数 returns。这就是我们在堆上分配东西的主要原因。
还有其他原因。
有时,class 的确切大小在编译期间是未知的。如果不知道 class 的确切大小,则无法在堆栈上创建它,因为编译器需要准确了解它需要为堆栈上的每个项目分配多少 space 。
此外,像whatever::createEntity()
这样的工厂方法也经常被使用。如果您必须调用一个单独的方法来为您创建一个对象,那么该对象将无法在堆栈上创建,原因已在本答案的第一段中解释。
堆分配不可避免地比堆栈分配慢得多。更多关于 "How much slower?" 稍后。然而,在很多情况下,选择是 "made for you",原因如下:
- 堆栈有限。如果你 运行 退出,应用程序几乎总是被终止 - 没有真正好的恢复,甚至打印一条错误消息说 "I ran out of stack" 可能很难......
- 堆栈分配 "goes away" 当您离开进行分配的函数时。
- 可变性更加明确且易于处理。 C++ 不能很好地处理 "variable length arrays" ,当然不能保证它在所有编译器中都能工作。
堆比堆栈慢多少?
我们稍后会谈到 "and does it matter"。
对于给定的分配,堆栈分配只是一个减法运算 [1],其中至少 new
或 malloc
将是一个函数调用,甚至可能是最简单的分配器将是几十条指令,在复杂的情况下是数千条[因为必须从 OS 获取内存,并清除它以前的内容]。因此,从 10 倍到 "infinitely" 的任何慢速,给予或接受。确切的数字将取决于代码 运行 进入的确切系统、分配的大小,并且通常 "previous calls to the allocator" (例如,一长串 "freed" 分配会使分配新对象的速度变慢,因为必须寻找合适的)。当然,除非你执行堆管理的 "ostrich" 方法,否则你还需要释放对象并应对 "out of memory" 这会增加更多 code/time 代码的执行和复杂性。
然而,通过一些相当聪明的编程,这在很大程度上可以被隐藏——例如,分配一些在对象的整个生命周期中长时间保持分配状态的东西,将是 "nothing to worry about"。在 3D 游戏中为每个像素或每个三角形从堆中分配对象显然不是一个好主意。但是如果对象的生命周期是很多帧甚至是整个游戏,那么分配和释放它的时间几乎为零。
同样,不是进行 10000 个单独的对象分配,而是为 10000 个对象分配一个。对象池就是这样一个概念。
此外,通常分配时间不在花费时间的地方。例如,从磁盘的文件中读取一个三角形列表比为同一个三角形列表分配 space 花费的时间要长得多——即使你分配了每个三角形列表!
对我来说,规则是:
- 它适合堆栈吗?通常几千字节就可以了,很多千字节不太好,而兆字节肯定不行。
- 数量(例如对象数组)是否已知,以及可以将其放入堆栈的最大值?
- 你知道对象是什么吗?换句话说 abstract/polymorphic 类 可能需要在堆上分配。
- 它的生命周期和它所在的范围一样吗?如果不是,则使用堆(或进一步向下堆栈,然后向上传递堆栈)。
[1] 或者如果堆栈是 "grows towards high addresses" 添加 - 我不知道有这样的架构的机器,但它是可以想象的,我认为已经制造了一些。 C 当然不承诺堆栈的增长方式,或有关 运行 时间堆栈如何工作的任何其他内容。
为什么是指针而不是对象?
因为指针有助于加快速度。如果按值将对象传递给另一个函数,例如
shoot(Orge::Entity ogre)
而不是
shoot(Orge::Entity* ogrePtr)
如果 ogre 不是指针,那么您会将整个对象而不是引用传递给函数。如果编译器不优化,您将得到一个低效的程序。还有其他原因,使用指针,您可以修改传入的对象(一些人认为引用更好,但这是一个不同的讨论)。否则您将花费太多时间来回复制修改后的对象。
为什么堆?
- 从某种意义上说,堆是一种更安全的内存类型,可以让您安全地访问 reset/recover。如果您调用 new 并且没有内存,则可以将其标记为错误。如果你正在使用堆栈,实际上没有什么好的方法可以知道你是否导致了 Whosebug,没有其他一些监督程序,此时你已经处于危险区域。
- 取决于您的应用。 Stack 具有本地范围,因此如果对象超出范围,它将为该对象释放内存。如果您需要其他功能中的对象,则没有真正的方法可以做到这一点。
- 更适用于OS,堆比堆栈大很多,尤其是在多线程应用中,每个线程的堆栈大小都是有限的。
我最近投身于图形编程,我注意到许多图形引擎(即 Ogre)和总体上许多编码人员更喜欢动态初始化 class 实例。这是来自 Ogre Basic Tutorial 1
的示例//...
Ogre::Entity* ogreHead = mSceneMgr->createEntity("Head", "ogrehead.mesh");
Ogre::SceneNode* headNode = mSceneMgr->getRootSceneNode()->createChildSceneNode("HeadNode");
//...
ogreHead
和 headNode
数据成员和方法被称为 ogreHead->blabla
.
为什么要乱用对象指针而不是普通对象?
顺便说一句,我还在某处读到堆内存分配比堆栈内存分配慢得多。
栈的范围是有限的:它只存在于一个函数中。现在,现代用户界面程序通常是事件驱动的,这意味着调用您的函数来处理事件,然后该函数必须 return 才能使程序继续 运行。因此,如果您的事件处理程序函数希望创建一个对象,该对象将在函数 returned 后继续存在,显然,该对象不能分配到该函数的堆栈上,因为它很快就会不复存在作为函数 returns。这就是我们在堆上分配东西的主要原因。
还有其他原因。
有时,class 的确切大小在编译期间是未知的。如果不知道 class 的确切大小,则无法在堆栈上创建它,因为编译器需要准确了解它需要为堆栈上的每个项目分配多少 space 。
此外,像whatever::createEntity()
这样的工厂方法也经常被使用。如果您必须调用一个单独的方法来为您创建一个对象,那么该对象将无法在堆栈上创建,原因已在本答案的第一段中解释。
堆分配不可避免地比堆栈分配慢得多。更多关于 "How much slower?" 稍后。然而,在很多情况下,选择是 "made for you",原因如下:
- 堆栈有限。如果你 运行 退出,应用程序几乎总是被终止 - 没有真正好的恢复,甚至打印一条错误消息说 "I ran out of stack" 可能很难......
- 堆栈分配 "goes away" 当您离开进行分配的函数时。
- 可变性更加明确且易于处理。 C++ 不能很好地处理 "variable length arrays" ,当然不能保证它在所有编译器中都能工作。
堆比堆栈慢多少?
我们稍后会谈到 "and does it matter"。
对于给定的分配,堆栈分配只是一个减法运算 [1],其中至少 new
或 malloc
将是一个函数调用,甚至可能是最简单的分配器将是几十条指令,在复杂的情况下是数千条[因为必须从 OS 获取内存,并清除它以前的内容]。因此,从 10 倍到 "infinitely" 的任何慢速,给予或接受。确切的数字将取决于代码 运行 进入的确切系统、分配的大小,并且通常 "previous calls to the allocator" (例如,一长串 "freed" 分配会使分配新对象的速度变慢,因为必须寻找合适的)。当然,除非你执行堆管理的 "ostrich" 方法,否则你还需要释放对象并应对 "out of memory" 这会增加更多 code/time 代码的执行和复杂性。
然而,通过一些相当聪明的编程,这在很大程度上可以被隐藏——例如,分配一些在对象的整个生命周期中长时间保持分配状态的东西,将是 "nothing to worry about"。在 3D 游戏中为每个像素或每个三角形从堆中分配对象显然不是一个好主意。但是如果对象的生命周期是很多帧甚至是整个游戏,那么分配和释放它的时间几乎为零。
同样,不是进行 10000 个单独的对象分配,而是为 10000 个对象分配一个。对象池就是这样一个概念。
此外,通常分配时间不在花费时间的地方。例如,从磁盘的文件中读取一个三角形列表比为同一个三角形列表分配 space 花费的时间要长得多——即使你分配了每个三角形列表!
对我来说,规则是:
- 它适合堆栈吗?通常几千字节就可以了,很多千字节不太好,而兆字节肯定不行。
- 数量(例如对象数组)是否已知,以及可以将其放入堆栈的最大值?
- 你知道对象是什么吗?换句话说 abstract/polymorphic 类 可能需要在堆上分配。
- 它的生命周期和它所在的范围一样吗?如果不是,则使用堆(或进一步向下堆栈,然后向上传递堆栈)。
[1] 或者如果堆栈是 "grows towards high addresses" 添加 - 我不知道有这样的架构的机器,但它是可以想象的,我认为已经制造了一些。 C 当然不承诺堆栈的增长方式,或有关 运行 时间堆栈如何工作的任何其他内容。
为什么是指针而不是对象?
因为指针有助于加快速度。如果按值将对象传递给另一个函数,例如
shoot(Orge::Entity ogre)
而不是
shoot(Orge::Entity* ogrePtr)
如果 ogre 不是指针,那么您会将整个对象而不是引用传递给函数。如果编译器不优化,您将得到一个低效的程序。还有其他原因,使用指针,您可以修改传入的对象(一些人认为引用更好,但这是一个不同的讨论)。否则您将花费太多时间来回复制修改后的对象。
为什么堆?
- 从某种意义上说,堆是一种更安全的内存类型,可以让您安全地访问 reset/recover。如果您调用 new 并且没有内存,则可以将其标记为错误。如果你正在使用堆栈,实际上没有什么好的方法可以知道你是否导致了 Whosebug,没有其他一些监督程序,此时你已经处于危险区域。
- 取决于您的应用。 Stack 具有本地范围,因此如果对象超出范围,它将为该对象释放内存。如果您需要其他功能中的对象,则没有真正的方法可以做到这一点。
- 更适用于OS,堆比堆栈大很多,尤其是在多线程应用中,每个线程的堆栈大小都是有限的。