研究!rsc公司

关于编程的想法和链接,通过

RSS(RSS)

围棋中的破碎抽象
发布于2010年3月29日,星期一。

我最喜欢的Go之一是这个推迟声明,因为它似乎作弊。它以如下方式重用函数调用代码:添加只是其中的几行编译器后端,比原本要短得多如果从头开始实施。但是重用函数调用代码需要破坏抽象层以一种不太明显的方式出现。推迟用一些更有趣的方式作弊也是。(我并没有声称对这些聪明的代码负责。我相信这都是因为肯·汤普森。)

去吧

在Go中,语句转到f(x,y,z)开始新的goroutine运行函数调用f(x,y,z).的值(f),x,,z(z)都是在原始goroutine中计算的:这只是执行(f)发生在新的高卢人。

在我们继续之前,停下来想想你会怎么做如果您正在生成C代码,请实现此功能。我可能会创建一个包含字段的结构(f),x,、和z(z),然后我会编写一个函数,将指针指向该结构作为输入并调用f(x,y,z)然后我会编译语句,通过生成代码来分配和填充结构,然后调用函数go(helper,structptr)哪里帮手是执行调用的另一个函数。这是一个相当大的工作量,它必须发生在每次通话。Go的实现避免所有这些。

让我们推迟看一看正常人函数调用。这里是呼叫集合f(1,2,3):

移动$1,0(SP)移动$2,4(SP)MOVL$3,8(标准普尔)呼叫f(SB)

它可以存储1,2、和在堆栈上(f)期望它们,然后执行呼叫说明。足够简单。

这是的代码转到f(1,2,3):

移动$1,0(SP)移动$2,4(SP)平均$3,8(SP)PUSHQ$f(SB)PUSHQ 12美元CALL runtime.newproc(SB)POPQ AX系列POPQ AX系列

这不是你可以用C写的东西。它从设置普通函数调用开始,将参数存储在常用位置。但在最后一刻,它突然转向:而不是呼叫(f)直接再推两个堆栈上的参数:(f)和数字12,然后它调用不同的函数运行时.newproc.

Newproc公司预期其参数为字节计数n个,要调用的函数,然后n个函数的字节参数,已布局函数需要如何接收它们。它复制了那些n个字节到新堆栈开始(f)使用堆栈指针运行指向那些论点。新工艺本质上是帮助者,但只有一个例子。

在gc编译器(6g等)中调用与相同的代码生成器对于普通函数调用,但它以5指令序列而不是简单指令序列呼叫说明。

这个技巧消除了我会生成的所有特殊代码,在运行时只需要几行代码编译器中只有少数几行。

推迟

声明延迟f(x,y,z)就像是转到f(x,y,z),但它没有在新的goroutine中运行呼叫,而是节省了调用later,在当前函数最终返回时运行它。声明本身的实现几乎是与的实现相同,除了生成的代码调用延迟程序而不是新程序.

有趣的新破抽象推迟是如何调用延迟函数的。如果函数包含defer语句,而不是以结尾通常的尾声

ADDL$48,SP//或任何框架尺寸转塔

函数以结尾

CALL runtime.deferreturn(SB)ADDL$48,标准普尔转塔

运行时函数延期归还安排运行延迟的调用(如果有的话)。怎么用?每个goroutine都与它关联了一个链接延迟调用列表,推迟结构:

结构延迟{int32大小;字节*sp;字节*fn;延迟*链接;字节参数[8];//填充到实际大小};

该结构表示对的延迟调用fn公司使用尺寸存储在中的参数字节参数.(结构在末尾分配了足够的空间即使尺寸>8.)这个链接字段用于链接列表。关于服务提供商?就是这样延期催缴股款知道是不是时候了运行特定呼叫。这个服务提供商字段记录第一个参数(后面fn公司)在呼叫中延迟程序.如果第一个参数为延期催缴股款位于相同的地址,然后推迟是为了这个调用帧。如果不是,则用于不同的调用帧,由于函数退出时会运行延迟,它必须用于堆栈上方的帧:

空隙延迟返回(uintptr arg0){字节*sp,*fn;延迟*d;sp=(字节*)&arg0;d=g->推迟;如果(d==零||d->sp!=服务提供商)回报;mcpy(d->sp,d->args,d->siz);g->defer=d->链接;fn=d->fn;免费(d);jmpdefer(fn,sp);}

如果你用纯C语言写这个,你可能已经每个调用帧或入口的单独堆栈到具有推迟声明你会将一个特殊标记推到延迟堆栈上,然后然后,在最后,运行延迟调用,直到找到标记为止。但如果你在机器指令的底层世界工作,堆栈指针是一个非常好的唯一标识符特定呼叫帧的;使用它可以避免任何在进入函数的过程中工作。

如果有要运行的延迟调用,则延期归还把论点复制到堆里-肯定有空间,因为那是同一个地址延迟程序从defer存根中复制它们,然后调用汇编函数jmpdefer公司以转移控制到fn公司就像原始函数一样打过电话fn公司直接代替延期归还.

但是等等!这只处理单个延迟呼叫,然而,函数可以在过程中延迟许多调用其执行情况。那怎么可能呢?

嗯,组装蹦床jmpdefer公司还有一个抽象破坏者。它减去五个大小呼叫调用的指令延期归还-来自堆栈上的返回地址跳到之前fn公司,因此延迟函数不返回指令之后这个呼叫像往常一样,但回到呼叫自身。也就是说,减去五圈呼叫指令进入一个循环。唯一的出路是延期归还找不到剩下的工作并正常返回,不打电话jmpdefer公司.这个花招避免了编写循环的需要在每个函数的末尾使用defer语句。

论抽象

在今天的编程世界中,似乎有很多强调抽象的力量。我认为对打破抽象。这三个地方都有围棋实现破坏了抽象比在抽象边界。

这三种技巧在Unix早期很常见,因为它是在汇编中编写的。例如,原始系统调用处理程序已区分通过更改返回地址从子级变成父级正如jmpdefer公司做。在现代Unix中系统调用返回父进程中的新进程id,但返回零在孩子身上。在早期版本中,包括第六版,返回两者中的新进程id,但孩子恢复了正常而父级返回到一条指令超过通常的回信地址。因此,调用fork系统调用后的指令需要无条件跳转到儿童特定代码。

围棋中还有其他更基本的抽象概念。Go中分段堆栈的实现打破了堆栈的简单抽象大多数C编译器都假设(在另一篇文章中有更多介绍)。对象可以实现接口,而无需显式声明该事实与Java无关:无法编译Go到标准Java字节码,因为Go的接口破坏了JVM的抽象。

最终,我认为我喜欢这些破碎的东西的原因抽象是它们帮助您获得更好地理解整个系统。之前你只看到了两个不同的层,现在您开始了解层是如何关联的以及他们如何互动。每一个破碎的抽象都是一个机会看到或创建一个新概念以前很容易表达。

(评论最初通过Blogger发布。)

  • 巴里·凯利 (2010年3月29日上午9:49)从代码中删除抽象显然是编译器的工作。最终,它需要删除所有抽象,直到剩余的比特流能够向硬件发出信号,以在物理世界中生成所需的效果。

    所以,是的,当你实现一个编译器时,你需要意识到一个事实,那就是你的工作就是破坏抽象,把它映射到正确性允许的适当层。但这与打破抽象是件好事不同。

  • 自残性窒息 (2010年3月29日上午9:57)堆栈上传递的参数也是64位的吗?

    call/ret-5诡计如何影响现代处理器上的call/ret预测?

  • 俄罗斯考克斯 (2010年3月29日上午10:21)@巴里:如果一直这么做,那不是一件好事,但偶尔也会是件好事。

    @aa:f调用了deferreturn,它调用了jmpdefer,它直接返回到f,这已经打破了RET的预测。倒带CALL指令不会使情况变得更糟。;-)

  • 彼得 (2010年5月19日9:21 PM)我非常确信,通过充分利用Java接口,您可以编译Go to JVM。这肯定不会很好看-D类

  • I可拆卸 (2011年1月4日下午5:30)为什么go-helper runtime.newproc不自动弹出known-to-be函数地址和arglist字节大小,而不是依赖调用方弹出它们?这将扭转局面:

    移动$1,0(SP)
    移动$2,4(SP)
    平均$3,8(SP)
    PUSHQ$f(SB)
    PUSHQ 12美元
    CALL runtime.newproc(SB)
    POPQ AX系列
    POPQ AX系列

    进入之内

    移动$1,0(SP)
    移动$2,4(SP)
    MOVL$3,8(标准普尔)
    PUSHQ$f(SB)
    PUSHQ 12美元
    CALL runtime.newproc(SB)

    哪种管道尺寸更好?

  • 卡斯滕·米尔考 (2011年5月27日上午5:08)我想知道为什么

    defer<函数体>
    转到<FunctionBody>

    还没有变成(词汇上的)同义词

    defer函数()
    go func()<FunctionBody>()

    遵循go的一般设计模式以避免样板。

    IMHO这是相当直接和明确的,记住匿名函数是闭包。defer和go关键字非常突出,足以提醒这是一个匿名函数调用,而不是一个普通的块。然而,在许多情况下,这种差异并不重要,在这些情况下,相似性甚至是一个优点。