【问题标题】:Why did C never implement "stack extension"?为什么 C 从未实现“堆栈扩展”?
【发布时间】:2018-06-25 01:04:35
【问题描述】:

为什么 C 从未实现“堆栈扩展”以允许调用者引用被调用函数的(动态大小的)堆栈变量?

这可以通过扩展调用者的堆栈帧以包含来自被调用者的堆栈帧的“动态返回”变量来实现。 (您可以但不应该使用来自调用者的alloca 来实现它 - 它可能无法在优化中存活。)

例如如果我想返回动态大小的字符串“e”,实现可以是:

--+---+-----+
  | a |  b  |
--+---+-----+

callee(d);

--+---+-----+---------+---+
  | a |  b  |  junk   | d |    
--+---+-----+---------+---+

char e[calculated_size];

--+---+-----+---------+---+---------+
  | a |  b  |  junk   | d |    e    |    
--+---+-----+---------+---+---------+

dynamic_return e;

--+---+-----+-------------+---------+
  | a |  b  |    waste    |    e    |
--+---+-----+-------------+---------+

(“垃圾”包含程序不可见的返回地址和其他系统特定的元数据。)

使用时会浪费一点堆栈空间。

好处是简化了字符串处理,以及任何其他当前必须malloc ram 的函数,返回指针并希望调用者在正确的时间记住free

显然,在 C 生命的这个阶段添加这样一个特性是没有意义的,我只是想知道为什么这不是一个好主意。

【问题讨论】:

  • 那将是低效的。一切都是权衡 - 当你在一侧得到一些改进时 - 然后检查另一侧的速度较慢。是的,这是来自实施者的说明。标准不使用堆栈一词。
  • 原因可能是因为在 C 编程语言中没有“堆栈”之类的东西。
  • @Lundin 是否有任何重要的“无堆栈”C 实现?
  • 是的,有几个。例如,飞思卡尔/恩智浦 RS08 没有堆栈,但受 C 编译器支持。无论如何,我的观点是,C 标准的目的不是规定堆栈帧的格式和调用约定。这超出了编程语言标准的范围。
  • 在所有可以用 C 实现的伟大事物中,这一点可能是无关紧要的。

标签: c stack


【解决方案1】:

对你的想法有一些反对意见。在 cmets 中已经提到了一些。有些来自我的脑海。

  • C 没有堆栈或堆栈帧。 C 简单地定义了范围及其生命周期,而如何实现标准则留给实现。堆栈和堆栈帧实际上只是实现某些 C 语义的最流行的方式。

  • C 没有字符串。 C实际上并没有这样的数组。好吧,它确实有数组,但是一旦您在表达式(例如返回表达式)中提到数组,数组就会衰减为指向其第一个元素的指针。返回堆栈上的“字符串”或数组将对语言的成熟领域产生重大影响。

  • C 确实有structs。但是,您已经可以返回 struct。我不能告诉你它是如何完成的,因为它是一个实现细节。

  • 具体实现的一个问题是调用者必须知道“浪费”有多大。不要忘记浪费将包括被调用者的堆栈帧,还包括被调用者直接或间接调用的任何函数的浪费。返回约定必须包含有关废物大小的信息和指向返回值的指针。

  • 与堆内存相比,堆栈通常非常有限,尤其是在使用线程的应用程序中。在某些时候,调用者需要将返回的数组向下移动到自己的堆栈帧中。如果数组只是一个指向堆中存储的指针,那效率会高得多,但是你已经有了现有的模型。

【讨论】:

  • 一些编译器可能能够优化到删除任何堆栈的程度(例如,通过在链接时优化时内联每个函数)。
  • @BasileStarynkevitch 是的。我认为我的第一点已经涵盖了这一点。不一定有堆栈。
  • 可能,但 OP 可能没有考虑到这一点。
  • “C 并没有这样的数组”:抱歉,这是完全错误的。 C确实有数组,数组可以静态分配,自动分配,成为structs的一部分,甚至是数组的元素。 C 知道并处理像int (*)[n][m] 这样的类型。 array-pointer-decay 只是 C 用来访问 数组元素、像通过引用一样传递数组以及创建多维数组作为包含数组作为元素的数组的特殊情况的机制。
  • @cmaster 请阅读整段而不是引用挖掘前七个单词。数组指针衰减机制没有“只是”。它是 C 对数组建模方式的核心。这正是您不能将数组作为参数传递或(与此问题相关)返回一个的原因。
【解决方案2】:

一个新的对象可以通过多层软件返回。所以浪费的空间可能来自几十个甚至几百个函数调用。

还要考虑一个执行某些迭代任务的例程。在每次迭代中,它从子例程中获取一些新分配的对象,然后将其插入到链表或其他数据结构中。这样的迭代任务可能会重复数百、数千或数百万次迭代。堆栈会因浪费的空间而溢出。

【讨论】:

    【解决方案3】:

    您必须意识到,堆栈的实现很大程度上受 CPU 和操作系统内核的支配。语言在这方面没有太多发言权。例如,限制是:

    • X86 架构的ret 指令期望返回地址位于堆栈指针中存储的内存位置。因此,顶部不能有其他任何东西(语义顶部 - 通常这是最低地址,因为堆栈往往会向下增长)。当然,您可以解决这个问题,但这可能会产生 C 程序员不愿意支付的额外开销。

    • 堆栈指针定义实际使用分配的堆栈内存的哪一部分。当控制流异步改变(硬件中断)时,当前 CPU 的寄存器通常会立即被中断处理程序存储到堆栈指针下方的内存地址中。这可能在任何时候发生,甚至在大多数内核代码中都可能发生。 存储在堆栈指针指向的位置下方的任何数据都将被此破坏。(嗯,从技术上讲,这并不完全正确,通常在堆栈指针下方有一个“红色区域”,中断处理程序可能不会写入任何数据。但在这里我们将非常深入地了解架构设计的特点。)

    • 销毁堆栈帧通常是向堆栈指针添加一个常量。这是你能得到的最快的一种指令,它通常不需要一个周期来执行(它将与一些内存访问并行执行)。如果堆栈帧具有动态大小,则必须通过从内存中加载堆栈指针来销毁堆栈帧,为此必须保留基指针。这是一个具有显着延迟的内存访问,并且必须保存另一个寄存器才能使用。同样,这是通常不必要的开销。

    您的提议肯定可以实施,但需要一些变通方法。这些变通办法通常会降低性能。少量的性能,但绝对是可衡量的。这不是编译器/内核开发人员想要的,而且有充分的理由。

    【讨论】:

    • “X86 架构期望返回地址位于栈顶” ...“存储在栈指针下方的任何数据都会被此破坏”值得指出的是,在 x86 和大多数其他现代架构,堆栈向下增长。栈顶是栈的最低使用地址。存储在堆栈指针下方(在内存中)的数据在技术上并不在堆栈上,并且有被破坏的危险,因为中断例程将堆栈指针向下移动到内存中以为寄存器腾出空间。
    • @JeremyP 很好。我现在已经更新了我的答案,以更准确地说明这一点。谢谢。
    • 我不会把它描述为一个问题,只是对一个好的答案的一个小的澄清。
    猜你喜欢
    • 1970-01-01
    • 2019-05-02
    • 2010-11-27
    • 1970-01-01
    • 1970-01-01
    • 2011-01-20
    • 1970-01-01
    • 1970-01-01
    • 2013-06-16
    相关资源
    最近更新 更多