在C和C ++之类的语言中,使用指向变量的指针时,我们还需要一个内存位置来存储该地址。那这不是内存开销吗?如何补偿?时间紧迫的低内存应用程序中使用指针吗?
在C和C ++之类的语言中,使用指向变量的指针时,我们还需要一个内存位置来存储该地址。那这不是内存开销吗?如何补偿?时间紧迫的低内存应用程序中使用指针吗?
Answers:
实际上,开销实际上并不在于存储指针所需的额外4或8个字节。大多数情况下,指针用于动态内存分配,这意味着我们调用一个函数来分配内存块,并且此函数向我们返回一个指向该内存块的指针。这个新块本身就代表了相当大的开销。
现在,您不必为了使用指针而进行内存分配:您可以int
静态地或在堆栈上声明数组,并且可以使用指针而不是索引来访问int
s,一切都非常好,简单高效。不需要内存分配,并且指针通常将在内存中的存储空间与整数索引所占用的空间一样多。
同样,正如约书亚·泰勒(Joshua Taylor)在评论中提醒我们的那样,指针用于通过引用传递某些内容。例如,struct foo f; init_foo(&f);
将在栈上分配f,然后init_foo()
使用指向该指针的指针进行调用struct
。这很常见。(请小心不要将这些指针“向上”传递。)在C ++中,您可能会看到使用“引用”(foo&
)而不是指针来完成此操作,但是引用不过是您不得更改的指针而已,它们占据了相同数量的内存。
但是使用指针的主要原因是用于动态内存分配,这样做是为了解决否则无法解决的问题。这是一个简单的示例:假设您要读取文件的全部内容。您要在哪里存放它们?如果尝试使用固定大小的缓冲区,则只能读取不超过该缓冲区的文件。但是通过使用内存分配,您可以分配必要的内存以读取文件,然后继续读取它。
而且,C ++是一种面向对象的语言,并且OOP的某些方面(例如抽象)只能使用指针来实现。甚至Java和C#之类的语言也 大量使用了指针,它们只是不允许您直接操作指针,以防止您对它们进行危险的操作,但是,一旦您有了这些语言,这些语言才有意义。意识到幕后的一切都是使用指针完成的。
因此,指针不只是在时间紧迫的,低内存的应用中,它们被用来随处可见。
struct foo f; init_foo(&f);
将f
在堆栈上分配,然后init_foo
使用指向该结构的指针进行调用。这很常见。(请注意不要将这些指针“向上”传递。)
malloc
已分配的块聚集在“存储桶”中时,头的开销非常低。另一方面,这通常会导致过度分配:您请求35个字节并获得64个字节(在您不知道的情况下),从而浪费了29 ...
那这不是内存开销吗?
当然,一个额外的地址(通常为4/8字节,取决于处理器)。
如何补偿?
它不是。如果您需要指针所需的间接访问,则可以为此付费。
时间紧迫的低内存应用程序中使用指针吗?
我在那里没有做很多工作,但我会做的。指针访问是汇编编程的基本方面。这需要微不足道的内存量,并且指针操作非常快速-即使在这类应用程序的上下文中也是如此。
我在这方面与Telastyn不太一样。
嵌入式处理器中的系统全局变量可以使用特定的硬编码地址来寻址。
程序中的全局变量将被视为与特殊指针的偏移量,该特殊指针指向内存中存储全局变量和静态变量的位置。
局部变量在输入函数时出现,并以与另一个特殊指针(通常称为“帧指针”)的偏移量进行寻址。这包括函数的参数。如果您对使用堆栈指针进行推入和弹出操作非常小心,则可以取消使用框架指针并直接从堆栈指针访问局部变量。
因此,无论您是遍历数组还是只是获取一些不起眼的局部或全局变量,都需要为指针的间接性付费。它只是基于不同的指针,具体取决于变量的种类。编译良好的代码会将指针保存在CPU寄存器中,而不是每次使用时都重新加载它。
当然是。但这是一种平衡的行为。
通常在构造低内存应用程序时要牢记一些指针变量的开销与大型程序(必须存储在内存中,请记住!)的开销之间的权衡(如果不能使用指针) 。
此注意事项适用于所有程序,因为没有人想用左右两边的重复代码构建可怕的,难以维护的混乱,这比需要的要大二十倍。
在C和C ++之类的语言中,使用指向变量的指针时,我们还需要一个内存位置来存储该地址。那这不是内存开销吗?
您假定指针需要存储。并非总是如此。每个变量都存储在某个内存地址。假设您有一个long
声明为long n = 5L;
。这会为n
某个地址分配存储空间。我们可以使用该地址来做一些精美的事情,例如*((char *) &n) = (char) 0xFF;
操纵的一部分n
。的地址n
不会存储在任何地方,这会产生额外的开销。
如何补偿?
即使将指针显式存储(例如,存储在列表等数据结构中),所得到的数据结构也比没有指针的等效数据结构更优雅(更简单,更易于理解,更易于处理等)。
时间紧迫的低内存应用程序中使用指针吗?
是。使用微控制器的设备通常只包含很少的内存,但是固件可能会使用指针来处理中断向量或缓冲区管理等。
gcc -fverbose-asm -S -O2
编译一些C代码)
拥有指针肯定会消耗一些开销,但是您也可以看到上行空间。指针就像索引一样。在C语言中,可以仅使用指针来使用复杂的数据结构,例如字符串和结构。
实际上,假设您要通过引用传递变量,那么它很容易维护指针,而不是复制整个结构并同步它们之间的更改(即使要复制它们,也需要使用指针)。没有指针,您将如何处理不连续的内存分配和取消分配?
甚至普通变量在符号表中都有一个条目,用于存储变量指向的地址。因此,我认为它不会在内存(仅4或8个字节)方面产生太多开销。即使像Java这样的语言在内部使用指针(引用),它们也不允许您操作它们,因为这会使JVM的安全性降低。
仅当没有其他选择(例如缺少数据类型,结构(在c中))时才应使用指针,因为如果处理不当,使用指针可能会导致错误,并且调试起来相对较难。
那这不是内存开销吗?
是的....不...也许吗?
这是一个尴尬的问题,因为想象一下机器上的内存寻址范围,以及一种需要以无法绑定到堆栈的方式持久地跟踪内存中内容的软件。
例如,假设有一个音乐播放器,当用户尝试加载另一个音乐文件时,该音乐文件由用户按下按钮加载,然后从易失性内存中卸载。
我们如何跟踪音频数据的存储位置?我们需要一个内存地址。该程序不仅需要跟踪内存中的音频数据块,还需要跟踪它在哪里在内存中的位置。因此,我们需要保留一个内存地址(即指针)。内存地址所需的存储空间将与机器的寻址范围相匹配(例如:64位寻址范围的64位指针)。
所以有点“是”,它确实需要存储来跟踪内存地址,但是对于这种动态分配的内存,我们似乎不能避免使用它。
如何补偿?
仅仅讨论指针本身的大小,在某些情况下可以利用堆栈来避免开销,例如,在那种情况下,编译器可以生成有效地对相对内存地址进行硬编码的指令,从而避免了指针的开销。但是,如果对大型可变大小的分配执行此操作,则容易受到堆栈溢出的影响,并且对于由用户输入驱动的一系列复杂的分支(如音频示例),这样做往往也是不切实际的(如果不是完全不可能的话)以上)。
另一种方法是使用更连续的数据结构。例如,可以使用基于数组的序列代替双向链接列表,该列表每个节点需要两个指针。我们还可以将两者混合使用,例如展开列表,该列表仅在N个元素的每个相邻组之间存储指针。
时间紧迫的低内存应用程序中使用指针吗?
是的,非常普遍,因为许多用C或C ++编写的性能关键型应用程序都由指针使用控制(它们可能位于智能指针或诸如此类的容器之后 std::vector
或std::string
,但是底层机制归结为所使用的指针以跟踪到动态内存块的地址)。
现在回到这个问题:
如何补偿?(第二部分)
指针通常非常便宜,除非您要存储一百万个指针(在64位计算机上仍然只有8 MB)。
*注意,正如Ben指出的那样,“适度”的8兆字节仍然是L3缓存的大小。在这里,我在总体DRAM使用情况和正常使用指针所指向的内存块的典型相对大小的意义上更多地使用了“中等”。
指针变得昂贵的地方不是指针本身,而是:
动态内存分配。动态内存分配往往很昂贵,因为它必须通过基础数据结构(例如:伙伴或平板分配器)。即使它们经常被优化以致死亡,它们还是通用的,旨在处理可变大小的块,这要求它们至少进行一些类似于“搜索”(尽管很轻,甚至可能是恒定时间)的工作,在内存中找到一组免费的连续页面。
内存访问。这往往是需要担心的更大开销。每当我们第一次访问动态分配的内存时,都会发生强制性页面错误以及高速缓存未命中,从而将内存向下移动到内存层次结构中并向下移动到寄存器中。
记忆体存取
内存访问是除算法之外性能最关键的方面之一。许多性能至关重要的领域(例如AAA游戏引擎)将大量精力集中在面向数据的优化上,这些优化可归结为更有效的内存访问模式和布局。
想要通过垃圾收集器分别分配每个用户定义类型的高级语言,最大的性能难题之一是它们可能使内存碎片很多。如果并非一次分配所有对象,则尤其如此。
在那些情况下,如果存储一百万个用户定义对象类型实例的列表,则在循环中顺序访问这些实例可能会非常慢,因为它类似于一百万个指向不同内存区域的指针的列表。在那些情况下,架构希望在对齐的大块中从较高,较慢,较大的层次结构中获取内存,希望在驱逐之前可以访问这些块中的周围数据。当这样一个列表中的每个对象分别分配时,当每次后续迭代可能都必须从内存中完全不同的区域加载且逐出之前没有相邻对象被访问时,我们通常最终会因高速缓存未命中而为此付出代价。
如今,许多此类语言的编译器在指令选择和寄存器分配方面做得非常好,但是这里缺乏对内存管理的更直接控制可能是致命的(尽管通常不那么容易出错),并且仍然使诸如C和C ++非常流行。
间接优化指针访问
在最关键的性能场景中,应用程序经常使用内存池,这些内存池从连续的块中池出内存以改善引用的局部性。在这种情况下,只要其节点的内存布局本质上是连续的,甚至可以使诸如树或链接列表之类的链接结构成为缓存友好的。这有效地使指针取消引用更便宜,尽管通过提高取消引用时所涉及的引用的位置可以间接地使其便宜。
追逐指针
假设我们有一个单链接列表,例如:
Foo->Bar->Baz->null
问题是,如果我们针对通用分配器(可能不是一次全部)分别分配所有这些节点,那么实际的内存可能会像这样分散(简化图):
当我们开始追逐指针并访问该Foo
节点时,我们从一个强制丢失(可能是页面错误)开始,将块从其内存区域从内存的较慢区域移动到内存的较快区域,如下所示:
这导致我们缓存(可能也分页)一个内存区域,以便仅访问该内存区域的一部分,而在我们追逐该列表周围的指针时,将其余区域逐出。但是,通过控制内存分配器,我们可以像下面这样连续分配这样的列表:
...从而显着提高了我们可以取消引用这些指针并处理它们的指针的速度。因此,尽管非常间接,但我们可以通过这种方式加快指针访问的速度。当然,如果只将它们连续存储在一个数组中,则首先不会出现此问题,但是这里的内存分配器为我们提供了对内存布局的明确控制,可以节省需要链接结构的日子。
*注意:这是一个过于简化的图表,并讨论了内存的层次结构和引用的局部性,但希望它适用于问题级别。
您只需要额外的内存使用量(通常每个指针4-8字节)就可以了。有许多技术可以使这种方法更加经济实惠。
使指针功能强大的最基本技术是,您不必保留每个指针。有时,您可以使用算法从一个指向其他对象的指针构造一个指针。最简单的例子是数组算法。如果分配一个由50个整数组成的数组,则无需保留50个指针,每个指针一个。通常,您会跟踪一个指针(第一个指针),并使用指针算法实时生成其他指针。有时,您可能会暂时保留指向数组特定元素的那些指针之一,但仅在需要时才保留。完成后,只要您保留了足够的信息以便以后重新生成,就可以将其丢弃。这听起来似乎微不足道,但这正是您所使用的保护工具
在内存非常紧张的情况下,可以使用它来最小化成本。如果你在一个非常狭窄的内存空间中工作,通常会对需要操作多少个对象有很好的了解。您可以利用开发人员的知识,在这种特定算法中永远不会拥有超过256个整数,而不是一次分配一堆整数并保持指向它们的完整指针。在这种情况下,您可能会保留指向第一个整数的指针,并使用char(1字节)而不是完整的指针(4/8字节)来跟踪索引。您可能还使用算法技巧来动态生成其中一些索引。
过去,这种记忆的尽责性非常流行。例如,NES游戏将在很大程度上依赖于其填充数据和通过算法生成指针的能力,而不必全部存储它们。
极端的内存情况也可能导致人们做一些事情,例如在编译时分配要操作的所有空间。然后,必须存储到该存储器的指针将存储在程序中,而不是数据中。在许多内存受限的情况下,您拥有单独的程序和数据存储器(通常是ROM与RAM),因此您可以调整使用算法将指针推入程序存储器的方式。
从根本上讲,您无法摆脱所有开销。但是,您可以控制它。通过使用算法技术,可以最大程度地减少可以存储的指针数量。如果碰巧使用了指向动态内存的指针,那么您将永远不会比保持1个指向该动态内存位置的指针的成本低,因为这是访问该内存块中任何内容所需的最少信息量。但是,在超紧内存约束情况下,这往往是特殊情况(动态内存和超紧内存约束往往不会在相同情况下出现)。