在C中使用callstack实现堆栈数据结构?
我对C语言下内存结构的理解是,程序的内存与堆栈和堆分开,每个堆栈和堆从块的两端开始增长,可以想象分配了所有ram,但显然抽象为某种操作系统内存片段管理器。在C中使用callstack实现堆栈数据结构?,c,assembly,stack,callstack,stack-memory,C,Assembly,Stack,Callstack,Stack Memory,我对C语言下内存结构的理解是,程序的内存与堆栈和堆分开,每个堆栈和堆从块的两端开始增长,可以想象分配了所有ram,但显然抽象为某种操作系统内存片段管理器。 设计用于处理局部变量的堆栈(自动存储)和用于内存分配的堆(动态存储) (编者按:有些C实现中,自动存储不使用“调用堆栈”,但这个问题假设在普通CPU上有一个普通的现代C实现,如果本地人不能只在寄存器中生存,他们就使用调用堆栈。) 假设我想为一些数据解析算法实现一个堆栈数据结构。其寿命和范围仅限于一个功能。 我可以想出3种方法来做这样的事情,
设计用于处理局部变量的堆栈(自动存储)和用于内存分配的堆(动态存储) (编者按:有些C实现中,自动存储不使用“调用堆栈”,但这个问题假设在普通CPU上有一个普通的现代C实现,如果本地人不能只在寄存器中生存,他们就使用调用堆栈。)
假设我想为一些数据解析算法实现一个堆栈数据结构。其寿命和范围仅限于一个功能。 我可以想出3种方法来做这样的事情,但在我看来,在这种情况下,没有一种方法是最干净的 <>强> >我的第一个是在堆中构建堆栈,如C++ >代码> STD::vector < /Calp> >:/P>
一些算法(一些数据)
{
标签*堆栈=新的堆栈(堆栈大小估计(数据));
迭代器i=某个迭代器(数据);
而(i)
{
Label Label=some_Label(在(i)处的some_迭代器);
if(标签\类型\ a(标签))
{
推送堆栈(堆栈、标签);
}
else if(标签\类型\ b(标签))
{
一些进程(数据、标签、pop_堆栈(堆栈));
}
i=某个迭代器(i);
}
一些堆栈清理(数据和堆栈);
删除_堆栈(stack);
返回数据;
}
这种方法是可以的,但它是浪费的,因为堆栈大小是一个猜测,并且在任何时候push_stack
都可能调用一些内部malloc或realloc,并导致不规则的减速。对于该算法来说,这些都不是问题,但这种结构似乎更适合于必须跨多个上下文维护堆栈的应用程序。这里的情况并非如此;堆栈对此函数是私有的,并且在退出之前被删除,就像自动存储类一样
我的下一个想法是递归。因为递归使用内置堆栈,这似乎更接近我想要的
一些算法(一些数据)
{
迭代器i=某个迭代器(数据);
返回一些额外的(算法辅助程序)(来自一些(数据)的额外的)&i;
}
额外算法(额外的东西,迭代器*i)
{
如果(!*i)
{返回事物;}
{
Label Label=some_Label(在(i)处的some_迭代器);
if(标签\类型\ a(标签))
{
*i=某个迭代器下一个(*i);
返回算法
(额外过程(算法辅助(事物,i),标签),i);
}
else if(标签\类型\ b(标签))
{
*i=某个迭代器下一个(*i);
返回额外的附件(物品、标签);
}
}
}
这种方法使我不用编写和维护堆栈。对我来说,代码似乎更难理解,这对我来说并不重要
我的主要问题是,这会占用更多的空间。堆栈帧包含这个
Extra
构造的副本(基本上包含一些数据
加上希望保存在堆栈中的实际位)和每个帧中完全相同的迭代器指针的不必要副本:因为它比引用一些静态全局指针“更安全”(我不知道如何不这样做)。如果编译器做了一些聪明的尾部递归之类的事情,这不会是一个问题,但我不知道我是否喜欢交叉手指,希望我的编译器很棒
我能想到的第三种方法是使用某种我不知道的C语言编写的最后一种动态数组,它可以在堆栈上生长。
或外部
asm
块
考虑到这一点,这正是我所寻找的,但我不认为我自己编写的asm版本非常简单,我也不认为它更容易编写或维护,尽管在我的脑海中它看起来更简单。显然,它在ISAs中是不可移植的
我不知道我是否忽略了某些功能,或者我是否需要找到另一种语言,或者我是否应该重新思考我的人生选择。一切都可能是真的……我希望这只是第一个
我不反对使用一些库。有吗?如果有,它是如何工作的?我在搜索中没有找到任何东西
我最近学习了可变长度数组,我真的不明白为什么不能利用它们作为增加堆栈引用的方法,但我也无法想象它们是这样工作的。这不是一个真正的答案,但对于一个简单的注释来说有点太长了 事实上,堆栈和堆的映像以及它们之间的相互增长过于简单化。8086处理器系列(至少在某些内存模型中)曾经是这样堆栈和堆共享一段内存,但即使是旧的Windows 3.1系统也提供了一些API函数,允许在堆外分配内存(搜索
GlobalAlloc
与LocalAlloc
相反),前提是处理器至少是80286
但是现代的系统都使用虚拟内存。有了虚拟内存,堆和堆栈不再共享一个很好的连续段,操作系统可以在任何地方提供内存(当然前提是它可以在某处找到可用内存)。但是堆栈仍然必须是一个连续的段。因此,该段的大小在构建时确定,并且是固定的,而堆的大小仅受系统可以分配给进程的最大内存的限制。这就是为什么许多人建议仅将堆栈用于小数据结构,并且始终是固定的此外,我知道没有一种可移植的方法可以让程序知道它的堆栈大小,更不用说它的空闲堆栈大小了
因此,IMHO,这里重要的是想知道堆栈的大小是否足够小。如果它可以超过一个小的限制,只需要分配内存,因为它会
#include <alloca.h>
#include <stdlib.h>
void some_func(char);
// assumptions:
// stack grows down
// alloca is contiguous
// all the UB manages to work like portable assembly language.
// input assumptions: no mismatched { and }
// made up useless algorithm: if('}') total += distance to matching '{'
size_t brace_distance(const char *data)
{
size_t total_distance = 0;
volatile unsigned hidden_from_optimizer = 1;
void *stack_base = alloca(hidden_from_optimizer); // highest address. top == this means empty
// alloca(1) would probably be optimized to just another local var, not necessarily at the bottom of the stack frame. Like char foo[1]
static const int growth_chunk = 128;
size_t *stack_top = stack_base;
size_t *high_water = alloca(growth_chunk);
for (size_t pos = 0; data[pos] != '\0' ; pos++) {
some_func(data[pos]);
if (data[pos] == '{') {
//push_stack(stack, pos);
stack_top--;
if (stack_top < high_water) // UB: optimized away by clang; never allocs more space
high_water = alloca(growth_chunk);
// assert(high_water < stack_top && "stack growth happened somewhere else");
*stack_top = pos;
}
else if(data[pos] == '}')
{
//total_distance += pop_stack(stack);
size_t popped = *stack_top;
stack_top++;
total_distance += pos - popped;
// assert(stack_top <= stack_base)
}
}
return total_distance;
}
# gcc9.2 -O1 -Wall -Wextra
# note that -O1 doesn't include some loop and peephole optimizations, e.g. no xor-zeroing
# but it's still readable, not like -O1 spilling every var to the stack between statements.
brace_distance:
push rbp
mov rbp, rsp # make a stack frame
push r15
push r14
push r13 # save some call-preserved regs for locals
push r12 # that will survive across the function call
push rbx
sub rsp, 24
mov r12, rdi
mov DWORD PTR [rbp-52], 1
mov eax, DWORD PTR [rbp-52]
mov eax, eax
add rax, 23
shr rax, 4
sal rax, 4 # some insane alloca rounding? Why not AND?
sub rsp, rax # alloca(1) moves the stack pointer, RSP, by whatever it rounded up to
lea r13, [rsp+15]
and r13, -16 # stack_base = 16-byte aligned pointer into that allocation.
sub rsp, 144 # alloca(128) reserves 144 bytes? Ok.
lea r14, [rsp+15]
and r14, -16 # and the actual C allocation rounds to %16
movzx edi, BYTE PTR [rdi] # data[0] check before first iteration
test dil, dil
je .L7 # if (empty string) goto return 0
mov ebx, 0 # pos = 0
mov r15d, 0 # total_distance = 0
jmp .L6
.L10:
lea rax, [r13-8] # tmp_top = top-1
cmp rax, r14
jnb .L4 # if(tmp_top < high_water)
sub rsp, 144
lea r14, [rsp+15]
and r14, -16 # high_water = alloca(128) if body
.L4:
mov QWORD PTR [r13-8], rbx # push(pos) - the actual store
mov r13, rax # top = tmp_top completes the --top
# yes this is clunky, hopefully with more optimization gcc would have just done
# sub r13, 8 and used [r13] instead of this RAX tmp
.L5:
add rbx, 1 # loop condition stuff
movzx edi, BYTE PTR [r12+rbx]
test dil, dil
je .L1
.L6: # top of loop body proper, with 8-bit DIL = the non-zero character
movsx edi, dil # unofficial part of the calling convention: sign-extend narrow args
call some_func # some_func(data[pos]
movzx eax, BYTE PTR [r12+rbx] # load data[pos]
cmp al, 123 # compare against braces
je .L10
cmp al, 125
jne .L5 # goto loop condition check if nothing special
# else: it was a '}'
mov rax, QWORD PTR [r13+0]
add r13, 8 # stack_top++ (8 bytes)
add r15, rbx # total += pos
sub r15, rax # total -= popped value
jmp .L5 # goto loop condition.
.L7:
mov r15d, 0
.L1:
mov rax, r15 # return total_distance
lea rsp, [rbp-40] # restore stack pointer to point at saved regs
pop rbx # standard epilogue
pop r12
pop r13
pop r14
pop r15
pop rbp
ret