我应该使用堆栈进行长期变量存储吗?

时间:2017-01-25 03:32:19

标签: assembly memory stack heap

根据"短期存储",第8章"汇编语言逐步" (第3版):

  

应该将堆栈视为短期存放东西的地方。存储在堆栈中的项目没有名称,并且通常必须按照与它们相反的顺序从堆栈中取出。最后,先出来,记住。 LIFO!

但是,根据我的知识,C编译器基本上都使用堆栈。这是否意味着堆栈是存储变量的最佳方式,包括短期和长期?或者有更好的方法吗?

我能想到的替代方案是:

  • 堆,但那很慢。
  • 静态变量,但这会持续整个程序的整个生命周期,这可能会浪费大量内存。

2 个答案:

答案 0 :(得分:2)

Stack通常用于将参数推送到函数调用,存储函数的局部变量,它还保留返回地址的轨迹(从当前函数返回后它将开始执行的指令)。但是,如何实现函数调用取决于编译器实现和calling conventions

  

C编译器基本上都使用堆栈

事实并非如此。 C编译器不会将全局和静态变量放在堆栈中。

  

这是否意味着堆栈是存储变量的最佳方式,包括短期和长期?

Stack应该用于当前函数返回后不会使用的变量。是的,您也可以长期使用堆栈。 main()中的局部变量将持续整个程序的生命周期。还要记住,每个程序的堆栈是有限的。

  

堆,但那很慢。

这是因为它需要在运行时进行一些管理。如果要在汇编中分配堆,则必须自己管理堆。在C,C ++等高级语言中,语言运行库和操作系统管理堆。你不会在集会中拥有它。

答案 1 :(得分:1)

C编译器基本上都使用堆栈。不是真的,有一些流行的指令集堆栈很重,因为do或didnt有很多寄存器。所以它部分是指令集的设计。一个理智的编译器设计将有一个调用约定,传递参数和返回信息的规则是什么。其中一些调用约定,在ISA中有很多寄存器,可能堆栈很重,或者可能会使用一些寄存器,然后在有很多参数时依赖堆栈。

然后你进入了在学校教授的程序员,像全局变量这样的东西是坏的。现在你有堆栈程序员的习惯,添加到功能的概念应该很小,适合打印12点字体的页面或适合你的屏幕,等等。这创造了大量的功能,所有功能都通过越来越多的参数通过许多嵌套函数,有时它是指向嵌套中高位的一个结构的指针,或者它的相同值或变量一次又一次地传递。由于函数嵌套的深度以及使用堆栈传递或存储变量,一些变量不仅存在很长时间而且存在很长时间,因此可能存在数十个或数百个该变量的副本。绝对与特定的编程语言无关,但在一定程度上与教育者的意见(在某些情况下有关于使文章评级更容易,而不一定是制作更好的程序)和习惯。

如果你有足够的寄存器并允许它们在调用约定中使用,并且你有一个优化器,你有时可以大大减少堆栈的使用量,程序员仍然在这里涉及他们的习惯并且仍然可能导致不必要的堆栈消耗和无法内联的嵌套仍然可能导致堆栈中的项目重复,或者在程序的整个生命周期中保留在堆栈上的结构或项目。

我喜欢调用本地全局变量的全局变量和静态局部变量在.data中不在堆栈中。有些程序员会在main()级别创建变量或结构,这些变量或结构在每个嵌套级别传递下去,耗费参数传递的消耗,如果它是一个堆栈繁重的调用约定,可以更有效地使用它,即使是通过引用传递你仍然在每个级别刻录指针,其中静态全局会更便宜,本地全局仍然会花费与顶级非静态本地相同的数量。你不能简单地说全局或静态本地人花费你更多,我认为他们的消费要少得多,取决于你的编程习惯和变量的选择,如果你创建一个新的变量,每个小的可能的东西确定你可以惹上麻烦。但是,例如,当您想要进行微控制器工作或其他非常受资源限制的嵌入式工作时,仅使用全局变量可以为您提供更好的成功机会,您的内存使用率几乎是固定的,您仍然可以获得存储空间嵌套函数的地址,不要内联。这有点极端,通过练习你可以使用本地人,你很有可能被优化到寄存器而不使用堆栈。程序员,处理器和编译器都依赖于重度本地使用或重度全局使用实际上是否消耗更少的内存。重度本地使用有可能只是临时使用,但对于受限系统,需要进行分析以确保您不会将堆栈崩溃到程序或堆中需要做更多工作来确保安全,您添加或删除的每行代码都可以当局部变量很重时,会对堆栈使用产生巨大影响。任何检测堆栈使用的方案都会立即花费大量资源来消耗更多的空间,而无需添加任何新的应用程序高级代码。

现在您正在阅读汇编语言书。不是编译器书。编译器程序员的习惯有点让人说限制或控制或其他一些词。为了调试输出并保持你的理智,你会看到编译器常常在前面和最后堆叠,基本上是堆栈帧。你不经常看到他们通过函数添加和删除所有东西导致相同项目的偏移更改,或者将另一个寄存器作为帧指针进行烧录,这样你就可以搞乱堆栈mid函数但整个函数中的一些局部变量x或传入变量y保持与该堆栈指针或帧指针相同的偏移量。汇编语言程序员也可以选择这样做,但也可以选择只使用堆栈作为一个相对短期的解决方案。

因此,以此为例,编写的代码强制编译器使用堆栈:

unsigned int more_fun ( unsigned int );
unsigned int fun ( unsigned int a )
{
    return(more_fun(a)+a+5);
}

创建

00000000 <fun>:
   0:   e92d4010    push    {r4, lr}
   4:   e1a04000    mov r4, r0
   8:   ebfffffe    bl  0 <more_fun>
   c:   e2844005    add r4, r4, #5
  10:   e0840000    add r0, r4, r0
  14:   e8bd4010    pop {r4, lr}
  18:   e12fff1e    bx  lr

使用堆栈帧方法,排序,在前端推送堆栈上的寄存器,并在后端释放/恢复它。然后使用该寄存器mid函数进行本地存储。这里的调用约定规定必须保留r4,所以下一个函数down保留并且所有嵌套都在下面,这样当我们回到这个函数时,r4是我们如何离开它(r0是参数进入并返回的内容)在这种情况下)是不稳定的,每个函数都可以破坏它。

虽然它违反了此指令集的当前约定,但您可以改为

push {lr}
push {r0}
bl more_fun
add r0,r0,#5
pop {r1}
add r0,r0,r1
pop {lr}
bx lr

比另一种方式更便宜,确保两个寄存器堆栈的推送和弹出比四个单独的便宜,对于这个指令集,我们不能做两次添加,我们使用相同数量的寄存器。在这种情况下编译器方法更便宜&#34;。但是如果编写的函数不必使用堆栈进行临时存储(取决于指令集)

,该怎么办?
unsigned int more_fun ( unsigned int );
unsigned int fun ( unsigned int a )
{
    return(more_fun(a)+5);
}

产生        0:e92d4010推{r4,lr}        4:ebfffffe bl 0        8:e8bd4010 pop {r4,lr}        c:e2800005添加r0,r0,#5       10:e12fff1e bx lr

然后你告诉我,但确实如此。部分调用约定,部分原因是如果总线是64位宽,现在通常用于ARM,或者即使不是,那么您要为一个事务添加一个时钟,该事务需要多达数百个时钟用于该附加寄存器,不是很大的成本,如果64位宽,那么单个寄存器推送和弹出实际上花费你不会省钱,同样在64位宽的总线上保持对齐64位边界,也节省了很多。在这种情况下编译器选择了r4,r4没有被保存在这里它只是编译器选择保持堆栈对齐的一些寄存器,正如您在其他与此相关的stackoverflow问题中看到的,有时编译器使用r3或其他寄存器,它选择了r4。

但超出了堆栈对齐和约定(我可以挖掘一个较旧的编译器来显示r4不在那里只是lr)。此代码不需要保留输入参数,以便在嵌套函数调用之后进行数学运算,在进入more_fun()之后,变量a可以被丢弃。

作为汇编语言程序员,你可能想要努力使用寄存器,我想这取决于指令集和你的习惯x86 CISC你可以直接在很多指令中使用内存操作数尽管有性能成本,养成习惯。但是如果你努力尽可能多地使用寄存器,你最终会陷入悬崖,并且使用了所有的寄存器并需要一个寄存器,所以你按照这本书的要求去做

push {r0}
ldr r0,[r2]
ldr r1,[r0]
pop {r0}

或类似的东西,用完寄存器,需要做双重间接。或者你可能需要一个中间变量而你只需要一个中间变量,所以你暂时使用堆栈

push {r0}
add r0,r1,r2
str r0,[r3]
pop {r0}

使用编译语言堆栈使用vs一些替代首先从处理器设计开始,是指令集缺乏通用寄存器,指令集是否使用堆栈设计用于函数调用指令并返回指令和中断以及中断返回或者他们是否使用注册表,让您选择是否需要将其保存在堆栈中。指令集是否基本上强制您进入堆栈使用,或者它是一个选项。接下来的编程习惯是他们自己教授或开发,可能导致堆栈使用繁重或更轻,函数太多,嵌套太多,单独的返回地址每次调用都会占用堆栈上的小字节,添加大量本地变量使用,根据功能大小,变量数量(可变大小)和函数中的代码,可以咀嚼更多或爆炸。如果你不使用优化器,那么你会得到大量的堆栈爆炸,你没有下降的悬崖效果,添加一个更多的行到一个函数从很少到没有堆栈使用到很多堆栈使用,因为你推了注册通过添加那一行来通过一个或多个悬崖使用。没有优化的堆栈消耗很重但更线性。使用寄存器是减少内存消耗的最佳方法,但在编码和查看编译器输出时需要大量练习,并希望下一个编译器以相同的方式工作,它们通常会这样做,但有时它们不会。你仍然可以编写你的代码,以便更加保守内存使用,并仍然完成任务。 (使用较小的变量,比如使用char而不是int,不一定能保存你,对于16,32和64位寄存器大小的指令集,它有时会花费额外的指令来签名扩展或屏蔽寄存器的其余部分。取决于指令集和你的代码)然后有全局,由于某种原因不满意,难以阅读?这很傻。他们有利有弊,你的消费更受控制,缺点是肯定的,如果你使用了很多变量,不再重复使用你会消耗更多的变量,而且它们在程序的生命周期中存在,他们不像非静态本地人那样自由自在。 static locals只是具有有限范围的全局变量,只有当你想要一个全局但是害怕被它避开时才使用它们,或者有一个非常具体的原因,其中有一个主要与递归相关的简短列表。

堆如何变慢? Ram通常是ram,如果你的变量在堆栈上或堆上,它需要相同的加载和存储来获取它,缓存命中和未命中,尽管你可以尝试操作,但它们有时候有时会遇到错过。有些处理器具有特殊的片上RAM用于堆栈确定,但那些不是我们今天看到的那种通用处理器,这些堆栈通常非常小。或者一些嵌入/裸机设计你可以将堆栈放在与.data或堆不同的ram上,因为你想使用它并让它拥有最快的内存。但是在你正在阅读它的机器上运行一个程序,程序,堆栈和.data /堆可能是同样缓慢的dram空间,一些缓存试图让它更快,但并非总是如此。 &#34;堆&#34;这是一个编译/操作系统使用内存无论如何,有分配和释放的问题,但一旦分配,那么性能与.text和.data相同,并且我们使用的很多目标平台的堆栈。使用堆栈,你基本上是在做一个malloc并且免费,而不是进行系统调用。但是你仍然可以像编译器使用上面的堆栈那样以有效的方式使用堆,一条指令用于推送和弹出两个东西,节省几个到几十个到几百个时钟周期。你可以使用malloc并减少更多的东西。当人们使用堆栈时没有意义(因为结构或数组的大小或结构数组),人们会这样做。