我应该使用堆栈来存储长期变量吗?
Should I use the stack for long-term variable storage?
根据 "Storage for Short Term","Assembly Language Step by Step"(第 3 版)中的第 8 章:
The stack should be considered a place to stash things for the short term. Items stored on the stack have no names, and in general must be taken off the stack in the reverse order in which they were put on. Last in, first out, remember. LIFO!
但是,据我所知,C 编译器基本上将堆栈用于所有操作。这是否意味着堆栈是存储短期和长期变量的最佳方式?或者有更好的方法吗?
我能想到的备选方案是:
- 堆,但是速度很慢。
- 静态变量,但这将持续整个程序的生命周期,这可能会浪费大量内存。
堆栈通常用于将参数推送到函数调用,存储函数的局部变量,它还跟踪 return 地址(在 returning 后它将开始执行的指令从当前功能)。但是,函数调用的实现方式取决于编译器实现和 calling conventions。
C compilers use the stack for basically everything
事实并非如此。 C 编译器不会将全局变量和静态变量放入堆栈。
Does that mean that the stack is the best way of storing variables, both short term and long term?
堆栈应该用于在当前函数 returns 之后不会使用的变量。是的,您也可以长期使用堆栈。 main()
中的局部变量将持续到程序的整个生命周期。另请记住,每个程序的堆栈都是有限的。
Heap, but that's slow.
那是因为它需要在运行时进行一些管理。如果你想在程序集中分配堆,你必须自己管理堆。在 C、C++ 等高级语言中,语言运行时和 OS 管理堆。你不会在汇编中有那个。
C 编译器基本上将堆栈用于所有事情。好吧,不是真的,有一些流行的指令集堆栈很重,因为 do 或 didn't 有很多寄存器。所以它部分是指令集的设计。一个理智的编译器设计将有一个调用约定,传递参数和 returning 信息的规则是什么。而其中一些调用约定,在ISA中是否有很多寄存器可能是堆栈繁重的或者可能使用一些寄存器然后在参数很多时依赖堆栈。
然后你就会明白程序员在学校里学到的东西,像全局变量这样的东西是不好的。现在你已经习惯了堆栈繁重的程序员,再加上函数的概念应该很小,适合 12 点字体的打印页面或适合你的屏幕等等。这会创建大量的函数,所有函数都通过许多嵌套函数,有时它是指向嵌套高层结构的指针,或者一遍又一遍地传递相同的值或它的变体。由于函数嵌套的深度以及使用堆栈来传递或存储变量,导致大量过度使用堆栈,一些变量不仅存在很长时间,而且可能有数十个或数百个该变量的副本。与特定的编程语言完全无关,但部分是教育工作者的意见(在某些情况下与使论文评分更容易而不一定是制作更好的程序有关)和习惯。
如果你有足够的寄存器并且你允许在调用约定中使用它们,并且你有一个优化器,你有时可以大大减少堆栈的使用量,程序员仍然可以按照他们的习惯参与其中导致不必要的堆栈消耗,并且无法内联的嵌套仍然会导致堆栈上的项目重复,或者结构或项目在程序的整个生命周期中都保留在堆栈上。
我喜欢称之为局部全局变量的全局变量和静态局部变量在 .data 中,而不是在堆栈中。有些程序员将在 main() 级别创建变量或结构,这些变量或结构将通过嵌套的每个级别向下传递,从而消耗参数传递的成本,如果它是一个堆栈繁重的调用约定,则可以更有效地使用它,即使通过引用传递你仍然在每个级别上燃烧一个指针,其中静态全局会便宜得多,本地全局仍然会花费你与顶级非静态本地相同的金额。你不能简单地说全局变量或静态局部变量会让你花费更多,我认为它们的消耗要少得多,这取决于你的编程习惯和变量的选择,如果你为每一个可能的小东西创建一个新名称的新变量,你肯定可以惹上麻烦。但是例如,当你想做微控制器工作或其他资源极度受限的嵌入式工作时,仅使用全局变量会给你更大的成功机会,你的内存使用几乎是固定的,你仍然有存储 return 嵌套且未内联的函数的地址。这有点极端,通过练习,您可以使用局部变量,您很有可能将其优化到寄存器中而不使用堆栈。大量的本地使用或大量的全局使用实际上消耗更少的内存,这在很大程度上取决于程序员、处理器和编译器。大量本地使用可能只是临时使用,但对于受限系统,确保您不会将堆栈崩溃到程序或堆中所需的分析需要做更多的工作来确保安全,您添加或删除的每一行代码都可以当大量使用局部变量时,会对堆栈使用产生显着影响。任何立即检测堆栈使用情况的方案都会让您消耗大量资源,在不添加任何新的应用程序高级代码的情况下消耗更多资源space。
现在您正在阅读一本汇编语言书籍。不是编译器的书。编译器程序员的习惯更像是受限或受控或其他词。为了调试输出并保持理智,您会看到编译器经常在前端和末尾弄乱堆栈,基本上是一个堆栈框架。你不会经常看到他们在整个函数中添加和删除东西,导致同一个项目的偏移量发生变化,或者将另一个寄存器作为帧指针燃烧,这样你就可以弄乱堆栈中间函数,但在整个函数中一些局部变量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
使用堆栈框架方法,有点像,在前端将一个寄存器压入堆栈,然后在后端释放它 up/restore。然后使用该寄存器中间函数进行本地存储。这里的调用约定规定必须保留 r4,因此下一个函数将保留以及下面的所有嵌套,以便当我们返回此函数时,r4 就是我们离开它的方式(r0 是参数的来源和 returns 在这种情况下)是易变的,每个函数都可以破坏它。
虽然它违反了该指令集的当前约定,但您可以改为
push {lr}
push {r0}
bl more_fun
add r0,r0,#5
pop {r1}
add r0,r0,r1
pop {lr}
bx lr
一种方式比另一种方式便宜,确保两个寄存器堆栈压入和弹出比四个单独的方式便宜,对于这个指令集,我们不能绕过两次加法,我们使用相同数量的寄存器。在这种情况下,编译器的方法是 "cheaper"。但是,如果编写的函数不必使用堆栈进行临时存储(取决于指令集)怎么办
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),或者即使不是,您也在为 t运行saction 添加一个时钟,而 t运行saction 需要许多到数百个时钟对于那个额外的寄存器,不是很大的成本,如果是 64 位宽,那么单个寄存器的压入和弹出实际上会花费你并不能节省你,同样地,当你有一个 64 位宽的总线时,在 64 位边界上保持对齐,也会为你节省很多.在这种情况下,编译器选择了 r4,r4 在这里没有被保留,它只是一些寄存器,编译器选择保持堆栈对齐,正如您在与此相关的其他 Whosebug 问题中看到的那样,有时编译器在此使用 r3 或其他寄存器如果它选择 r4.
但除了堆栈对齐和约定之外(我可以挖掘一个较旧的编译器来显示 r4 不只是 lr)。此代码不需要保留输入参数以便在嵌套函数调用后完成数学运算,在它进入 more_fun() 之后变量 a 可以被丢弃。
作为一个汇编语言程序员,你可能想要努力大量使用寄存器,我想这取决于指令集和你的习惯,你可以直接使用内存 ope运行ds 的 x86 CISC在许多说明中,尽管性能成本很高,但您可能会养成这样的习惯。但是如果你努力尽可能多地使用寄存器,你最终会掉下悬崖,所有的寄存器都用完了,还需要一个,所以你按照书上告诉你的去做
push {r0}
ldr r0,[r2]
ldr r1,[r0]
pop {r0}
或类似的东西,运行 超出寄存器,需要进行双重间接。或者你可能需要一个中间变量而你只剩下 none 备用,所以你暂时使用堆栈
push {r0}
add r0,r1,r2
str r0,[r3]
pop {r0}
使用编译语言堆栈使用与某些替代方案首先从处理器设计开始,指令集是否缺少通用寄存器,指令集是否通过设计为函数调用指令使用堆栈,return指令和中断和中断 returns 还是它们使用寄存器并让您选择是否需要将其保存在堆栈中。指令集是否强制您基本上使用堆栈,或者它是一个选项。下一个编程习惯,无论是他们教的还是你自己养成的,都会导致堆栈使用量大或小,函数太多,嵌套太多,单独的 return 地址每次调用都会在堆栈上占用很少的字节,添加大量使用局部变量,并且可以根据函数大小、变量数量(变量大小)和函数中的代码来咀嚼或分解它。如果你不使用优化器,那么你会得到大量的堆栈爆炸,你不会有向函数添加一行的悬崖效应从很少到没有堆栈使用到大量堆栈使用,因为你推了寄存器通过添加那条线,在悬崖上使用一个或多个。未优化的堆栈消耗很重但更线性。使用寄存器是减少内存消耗的最佳方法,但需要大量练习编码和查看编译器输出,并希望下一个编译器以 same-ish 方式工作,他们经常这样做,但有时却没有。您仍然可以编写代码以更加保守地使用内存并完成任务。 (使用较小的变量,例如使用 char 而不是 int 不一定会节省您的时间,对于 16、32 和 64 位寄存器大小的指令集,有时您需要额外的指令来签署扩展或屏蔽寄存器的其余部分。取决于指令集和您的代码)然后是全局变量,出于某种原因,它们不受欢迎,难以阅读?那是愚蠢的。他们有ros 和 cons 优点是你的消费更受控制,缺点是,如果你使用很多变量,不要 re-use 变量你会消耗更多,它们在程序的生命周期中存在, 他们不像 non-static 当地人那样自由自在。 static locals 只是范围有限的全局变量,只有当你想要一个全局变量但又害怕被拒绝时才使用它们,或者有一个非常具体的原因,其中有一个主要与递归相关的简短列表。
堆怎么变慢了? Ram 通常是 ram,如果你的变量在堆栈上或堆上,它需要相同的加载和存储来获取它,缓存命中和未命中,虽然你可以尝试操作,但它们仍然有时命中有时未命中。一些处理器具有特殊的片上 ram 用于堆栈确定,但这些不是我们今天看到的通用处理器类型,这些堆栈通常非常小。或者一些 embeded/bare 金属设计,你可以将堆栈放在与 .data 或堆不同的 ram 上,因为你想使用它并让它拥有最快的内存。但是在你正在阅读这篇文章的机器上拿一个程序,程序、堆栈和 .data/heap 可能是相同的慢 dram space,一些缓存试图让它更快,但并非总是如此。 "heap" 无论如何都是 compiled/operating 系统内存使用,存在分配和释放问题,但一旦分配,性能与 .text 和 .data 相同我们使用的目标平台。使用堆栈,您基本上是在执行 malloc 和 free,开销比进行系统调用要少。但是您仍然可以像上面的编译器使用堆栈一样以高效的方式使用堆,一条指令可以压入和弹出两个东西,从而节省几个到几十个甚至数百个时钟周期。你可以更少地 malloc 和释放更大的东西。当使用堆栈没有意义时(由于结构或数组或结构数组的大小),人们会这样做。
根据 "Storage for Short Term","Assembly Language Step by Step"(第 3 版)中的第 8 章:
The stack should be considered a place to stash things for the short term. Items stored on the stack have no names, and in general must be taken off the stack in the reverse order in which they were put on. Last in, first out, remember. LIFO!
但是,据我所知,C 编译器基本上将堆栈用于所有操作。这是否意味着堆栈是存储短期和长期变量的最佳方式?或者有更好的方法吗?
我能想到的备选方案是:
- 堆,但是速度很慢。
- 静态变量,但这将持续整个程序的生命周期,这可能会浪费大量内存。
堆栈通常用于将参数推送到函数调用,存储函数的局部变量,它还跟踪 return 地址(在 returning 后它将开始执行的指令从当前功能)。但是,函数调用的实现方式取决于编译器实现和 calling conventions。
C compilers use the stack for basically everything
事实并非如此。 C 编译器不会将全局变量和静态变量放入堆栈。
Does that mean that the stack is the best way of storing variables, both short term and long term?
堆栈应该用于在当前函数 returns 之后不会使用的变量。是的,您也可以长期使用堆栈。 main()
中的局部变量将持续到程序的整个生命周期。另请记住,每个程序的堆栈都是有限的。
Heap, but that's slow.
那是因为它需要在运行时进行一些管理。如果你想在程序集中分配堆,你必须自己管理堆。在 C、C++ 等高级语言中,语言运行时和 OS 管理堆。你不会在汇编中有那个。
C 编译器基本上将堆栈用于所有事情。好吧,不是真的,有一些流行的指令集堆栈很重,因为 do 或 didn't 有很多寄存器。所以它部分是指令集的设计。一个理智的编译器设计将有一个调用约定,传递参数和 returning 信息的规则是什么。而其中一些调用约定,在ISA中是否有很多寄存器可能是堆栈繁重的或者可能使用一些寄存器然后在参数很多时依赖堆栈。
然后你就会明白程序员在学校里学到的东西,像全局变量这样的东西是不好的。现在你已经习惯了堆栈繁重的程序员,再加上函数的概念应该很小,适合 12 点字体的打印页面或适合你的屏幕等等。这会创建大量的函数,所有函数都通过许多嵌套函数,有时它是指向嵌套高层结构的指针,或者一遍又一遍地传递相同的值或它的变体。由于函数嵌套的深度以及使用堆栈来传递或存储变量,导致大量过度使用堆栈,一些变量不仅存在很长时间,而且可能有数十个或数百个该变量的副本。与特定的编程语言完全无关,但部分是教育工作者的意见(在某些情况下与使论文评分更容易而不一定是制作更好的程序有关)和习惯。
如果你有足够的寄存器并且你允许在调用约定中使用它们,并且你有一个优化器,你有时可以大大减少堆栈的使用量,程序员仍然可以按照他们的习惯参与其中导致不必要的堆栈消耗,并且无法内联的嵌套仍然会导致堆栈上的项目重复,或者结构或项目在程序的整个生命周期中都保留在堆栈上。
我喜欢称之为局部全局变量的全局变量和静态局部变量在 .data 中,而不是在堆栈中。有些程序员将在 main() 级别创建变量或结构,这些变量或结构将通过嵌套的每个级别向下传递,从而消耗参数传递的成本,如果它是一个堆栈繁重的调用约定,则可以更有效地使用它,即使通过引用传递你仍然在每个级别上燃烧一个指针,其中静态全局会便宜得多,本地全局仍然会花费你与顶级非静态本地相同的金额。你不能简单地说全局变量或静态局部变量会让你花费更多,我认为它们的消耗要少得多,这取决于你的编程习惯和变量的选择,如果你为每一个可能的小东西创建一个新名称的新变量,你肯定可以惹上麻烦。但是例如,当你想做微控制器工作或其他资源极度受限的嵌入式工作时,仅使用全局变量会给你更大的成功机会,你的内存使用几乎是固定的,你仍然有存储 return 嵌套且未内联的函数的地址。这有点极端,通过练习,您可以使用局部变量,您很有可能将其优化到寄存器中而不使用堆栈。大量的本地使用或大量的全局使用实际上消耗更少的内存,这在很大程度上取决于程序员、处理器和编译器。大量本地使用可能只是临时使用,但对于受限系统,确保您不会将堆栈崩溃到程序或堆中所需的分析需要做更多的工作来确保安全,您添加或删除的每一行代码都可以当大量使用局部变量时,会对堆栈使用产生显着影响。任何立即检测堆栈使用情况的方案都会让您消耗大量资源,在不添加任何新的应用程序高级代码的情况下消耗更多资源space。
现在您正在阅读一本汇编语言书籍。不是编译器的书。编译器程序员的习惯更像是受限或受控或其他词。为了调试输出并保持理智,您会看到编译器经常在前端和末尾弄乱堆栈,基本上是一个堆栈框架。你不会经常看到他们在整个函数中添加和删除东西,导致同一个项目的偏移量发生变化,或者将另一个寄存器作为帧指针燃烧,这样你就可以弄乱堆栈中间函数,但在整个函数中一些局部变量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
使用堆栈框架方法,有点像,在前端将一个寄存器压入堆栈,然后在后端释放它 up/restore。然后使用该寄存器中间函数进行本地存储。这里的调用约定规定必须保留 r4,因此下一个函数将保留以及下面的所有嵌套,以便当我们返回此函数时,r4 就是我们离开它的方式(r0 是参数的来源和 returns 在这种情况下)是易变的,每个函数都可以破坏它。
虽然它违反了该指令集的当前约定,但您可以改为
push {lr}
push {r0}
bl more_fun
add r0,r0,#5
pop {r1}
add r0,r0,r1
pop {lr}
bx lr
一种方式比另一种方式便宜,确保两个寄存器堆栈压入和弹出比四个单独的方式便宜,对于这个指令集,我们不能绕过两次加法,我们使用相同数量的寄存器。在这种情况下,编译器的方法是 "cheaper"。但是,如果编写的函数不必使用堆栈进行临时存储(取决于指令集)怎么办
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),或者即使不是,您也在为 t运行saction 添加一个时钟,而 t运行saction 需要许多到数百个时钟对于那个额外的寄存器,不是很大的成本,如果是 64 位宽,那么单个寄存器的压入和弹出实际上会花费你并不能节省你,同样地,当你有一个 64 位宽的总线时,在 64 位边界上保持对齐,也会为你节省很多.在这种情况下,编译器选择了 r4,r4 在这里没有被保留,它只是一些寄存器,编译器选择保持堆栈对齐,正如您在与此相关的其他 Whosebug 问题中看到的那样,有时编译器在此使用 r3 或其他寄存器如果它选择 r4.
但除了堆栈对齐和约定之外(我可以挖掘一个较旧的编译器来显示 r4 不只是 lr)。此代码不需要保留输入参数以便在嵌套函数调用后完成数学运算,在它进入 more_fun() 之后变量 a 可以被丢弃。
作为一个汇编语言程序员,你可能想要努力大量使用寄存器,我想这取决于指令集和你的习惯,你可以直接使用内存 ope运行ds 的 x86 CISC在许多说明中,尽管性能成本很高,但您可能会养成这样的习惯。但是如果你努力尽可能多地使用寄存器,你最终会掉下悬崖,所有的寄存器都用完了,还需要一个,所以你按照书上告诉你的去做
push {r0}
ldr r0,[r2]
ldr r1,[r0]
pop {r0}
或类似的东西,运行 超出寄存器,需要进行双重间接。或者你可能需要一个中间变量而你只剩下 none 备用,所以你暂时使用堆栈
push {r0}
add r0,r1,r2
str r0,[r3]
pop {r0}
使用编译语言堆栈使用与某些替代方案首先从处理器设计开始,指令集是否缺少通用寄存器,指令集是否通过设计为函数调用指令使用堆栈,return指令和中断和中断 returns 还是它们使用寄存器并让您选择是否需要将其保存在堆栈中。指令集是否强制您基本上使用堆栈,或者它是一个选项。下一个编程习惯,无论是他们教的还是你自己养成的,都会导致堆栈使用量大或小,函数太多,嵌套太多,单独的 return 地址每次调用都会在堆栈上占用很少的字节,添加大量使用局部变量,并且可以根据函数大小、变量数量(变量大小)和函数中的代码来咀嚼或分解它。如果你不使用优化器,那么你会得到大量的堆栈爆炸,你不会有向函数添加一行的悬崖效应从很少到没有堆栈使用到大量堆栈使用,因为你推了寄存器通过添加那条线,在悬崖上使用一个或多个。未优化的堆栈消耗很重但更线性。使用寄存器是减少内存消耗的最佳方法,但需要大量练习编码和查看编译器输出,并希望下一个编译器以 same-ish 方式工作,他们经常这样做,但有时却没有。您仍然可以编写代码以更加保守地使用内存并完成任务。 (使用较小的变量,例如使用 char 而不是 int 不一定会节省您的时间,对于 16、32 和 64 位寄存器大小的指令集,有时您需要额外的指令来签署扩展或屏蔽寄存器的其余部分。取决于指令集和您的代码)然后是全局变量,出于某种原因,它们不受欢迎,难以阅读?那是愚蠢的。他们有ros 和 cons 优点是你的消费更受控制,缺点是,如果你使用很多变量,不要 re-use 变量你会消耗更多,它们在程序的生命周期中存在, 他们不像 non-static 当地人那样自由自在。 static locals 只是范围有限的全局变量,只有当你想要一个全局变量但又害怕被拒绝时才使用它们,或者有一个非常具体的原因,其中有一个主要与递归相关的简短列表。
堆怎么变慢了? Ram 通常是 ram,如果你的变量在堆栈上或堆上,它需要相同的加载和存储来获取它,缓存命中和未命中,虽然你可以尝试操作,但它们仍然有时命中有时未命中。一些处理器具有特殊的片上 ram 用于堆栈确定,但这些不是我们今天看到的通用处理器类型,这些堆栈通常非常小。或者一些 embeded/bare 金属设计,你可以将堆栈放在与 .data 或堆不同的 ram 上,因为你想使用它并让它拥有最快的内存。但是在你正在阅读这篇文章的机器上拿一个程序,程序、堆栈和 .data/heap 可能是相同的慢 dram space,一些缓存试图让它更快,但并非总是如此。 "heap" 无论如何都是 compiled/operating 系统内存使用,存在分配和释放问题,但一旦分配,性能与 .text 和 .data 相同我们使用的目标平台。使用堆栈,您基本上是在执行 malloc 和 free,开销比进行系统调用要少。但是您仍然可以像上面的编译器使用堆栈一样以高效的方式使用堆,一条指令可以压入和弹出两个东西,从而节省几个到几十个甚至数百个时钟周期。你可以更少地 malloc 和释放更大的东西。当使用堆栈没有意义时(由于结构或数组或结构数组的大小),人们会这样做。