【问题标题】:Should I use the stack for long-term variable storage?我应该使用堆栈进行长期变量存储吗?
【发布时间】:2017-06-10 02:56:32
【问题描述】:

根据“Storage for Short Term”,“Assembly Language Step by Step”(第 3 版)第 8 章:

堆栈应该被认为是短期存放东西的地方。存储在堆栈中的项目没有名称,通常必须以与放入时相反的顺序从堆栈中取出。后进先出,记住。后进先出!

但是,据我所知,C 编译器基本上所有事情都使用堆栈。这是否意味着堆栈是存储短期和长期变量的最佳方式?还是有更好的办法?

我能想到的替代方案是:

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

【问题讨论】:

  • ,7 秒对数据来说是永恒的。
  • 你的书是关于汇编语言的,而不是 C
  • @M.M 我明白,但我只是以 C 编译器的一般操作方式为例。
  • "存储在堆栈中的项目没有名称," -- 可以说对于普通装配来说是正确的,但是使用appropriate macros(或者即使只仔细使用标签),您可以为堆栈项目定义名称。示例in a depacker I ported to 8086 NASM source.

标签: assembly memory heap-memory stack-memory


【解决方案1】:

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

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

那不是真的。 C 编译器不会将全局变量和静态变量放入堆栈。

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

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

堆,但这很慢。

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

【讨论】:

  • 顺便说一句,如果您想了解更多关于从 c/c++ 代码生成汇编代码的信息,您可能会发现 gcc explorer 很有趣。
【解决方案2】:

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

然后你就会了解程序员在学校所教的东西,比如全局变量是不好的。现在你有堆栈重程序员的习惯,再加上函数的概念应该很小,适合 12 点字体的打印页面或适合你的屏幕等。这会创建大量的函数,它们都通过许多传递越来越多的参数嵌套函数,有时它是指向嵌套中较高的一个结构的指针,或者它的相同值或它的变体一遍又一遍地传递。由于函数嵌套的深度以及使用堆栈传递或存储变量,一些变量不仅存在很长时间,而且可能存在数十或数百个该变量的副本,从而导致堆栈的大量过度使用。与特定的编程语言完全无关,但部分与教育者的意见(在某些情况下与使论文评分更容易而不一定要制作更好的程序有关)和习惯有关。

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

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

现在您正在阅读一本汇编语言书籍。不是编译器的书。编译器程序员的习惯更多的是可以说受限或受控或其他词。为了调试输出并保持理智,您会看到编译器通常会在前面和结尾处弄乱堆栈,基本上是堆栈帧。您不会经常看到他们在整个函数中添加和删除东西,从而导致同一项目的偏移量发生变化,或者将另一个寄存器烧毁为帧指针,这样您就可以弄乱堆栈中间函数,但在整个函数中都有一些局部变量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

使用堆栈框架方法,有点像,在堆栈上预先压入一个寄存器,然后在后端释放/恢复它。然后使用该寄存器中间功能进行本地存储。这里的调用约定规定 r4 必须保留,因此下一个函数保留和下面的所有嵌套,这样当我们回到这个函数时,r4 是我们离开它的方式(r0 是参数进入并返回的内容在这种情况下) 是 volatile 每个函数都可以销毁它。

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

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

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

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

生产 0: e92d4010 推 {r4, lr} 4:ebffffffe bl 0 8: e8bd4010 弹出 {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}

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

堆有多慢? Ram 通常是 ram,如果您的变量在堆栈或堆上,它需要相同的加载和存储才能获取它,缓存命中和未命中,虽然您可以尝试操作,但它们有时会命中有时会丢失。一些处理器具有用于堆栈的特殊芯片内存,但这些不是我们今天看到的通用处理器,这些堆栈通常非常小。或者一些嵌入式/裸机设计,您可以将堆栈放在与 .data 或堆不同的 ram 上,因为您想使用它并让它拥有最快的内存。但是在你正在阅读这篇文章的机器上使用一个程序,程序、堆栈和 .data/heap 可能是相同的慢 dram 空间,一些缓存试图使其更快,但并非总是如此。无论如何,作为编译/操作系统使用内存的“堆”存在分配和释放问题,但一旦分配,性能与 .text 和 .data 以及我们的许多目标平台的堆栈相同采用。使用堆栈,您基本上是在执行 malloc 和 free ,而开销比进行系统调用要少。但是您仍然可以像编译器使用上面的堆栈一样有效地使用堆,一条指令推送和弹出两件事,节省几个到几十到几百个时钟周期。你可以更少地 malloc 和释放更大的东西。当使用堆栈没有意义时(因为结构或数组或结构数组的大小),人们会这样做。

【讨论】:

  • 嘿,感谢您提供的信息丰富的答案。你介意我帮忙清理一下吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-03-31
  • 2013-08-29
  • 1970-01-01
  • 2012-06-12
  • 2015-03-09
  • 2013-03-20
  • 1970-01-01
相关资源
最近更新 更多