整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Go 中对栈中函数进行内联 - Linux 中国

文中,我要论述内联的限制以及叶子内联与栈中内联mid-stack inlining的对比。

  • 来源:https://linux.cn/article-12184-1.html
  • 作者:Dave Cheney
  • 译者:Xiaobin.Liu

上一篇文章 中我论述了 叶子内联(leaf inlining)是怎样让 Go 编译器减少函数调用的开销的,以及延伸出了跨函数边界的优化的机会。本文中,我要论述内联的限制以及叶子内联与 栈中内联(mid-stack inlining)的对比。

内联的限制

把函数内联到它的调用处消除了调用的开销,为编译器进行其他的优化提供了更好的机会,那么问题来了,既然内联这么好,内联得越多开销就越少,为什么不尽可能多地内联呢?

内联可能会以增加程序大小换来更快的执行时间。限制内联的最主要原因是,创建许多函数的内联副本会增加编译时间,并导致生成更大的二进制文件的边际效应。即使把内联带来的进一步的优化机会考虑在内,太激进的内联也可能会增加生成的二进制文件的大小和编译时间。

内联收益最大的是 小函数 ,相对于调用它们的开销来说,这些函数做很少的工作。随着函数大小的增长,函数内部做的工作与函数调用的开销相比省下的时间越来越少。函数越大通常越复杂,因此优化其内联形式相对于原地优化的好处会减少。

内联预算

在编译过程中,每个函数的内联能力是用内联预算计算的 1 。开销的计算过程可以巧妙地内化,像一元和二元等简单操作,在 抽象语法数(Abstract Syntax Tree)(AST)中通常是每个节点一个单位,更复杂的操作如 make 可能单位更多。考虑下面的例子:

package main

func small() string {
    s := "hello, " + "world!"
    return s
}

func large() string {
    s := "a"
    s += "b"
    s += "c"
    s += "d"
    s += "e"
    s += "f"
    s += "g"
    s += "h"
    s += "i"
    s += "j"
    s += "k"
    s += "l"
    s += "m"
    s += "n"
    s += "o"
    s += "p"
    s += "q"
    s += "r"
    s += "s"
    s += "t"
    s += "u"
    s += "v"
    s += "w"
    s += "x"
    s += "y"
    s += "z"
    return s
}

func main() {
    small()
    large()
}

使用 -gcflags=-m=2 参数编译这个函数能让我们看到编译器分配给每个函数的开销:

% go build -gcflags=-m=2 inl.go
# command-line-arguments
./inl.go:3:6: can inline small with cost 7 as: func() string { s := "hello, world!"; return s }
./inl.go:8:6: cannot inline large: function too complex: cost 82 exceeds budget 80
./inl.go:38:6: can inline main with cost 68 as: func() { small(); large() }
./inl.go:39:7: inlining call to small func() string { s := "hello, world!"; return s }

编译器根据函数 func small() 的开销(7)决定可以对它内联,而 func large() 的开销太大,编译器决定不进行内联。func main() 被标记为适合内联的,分配了 68 的开销;其中 small 占用 7,调用 small 函数占用 57,剩余的(4)是它自己的开销。

可以用 -gcflag=-l 参数控制内联预算的等级。下面是可使用的值:

  • -gcflags=-l=0 默认的内联等级。
  • -gcflags=-l(或 -gcflags=-l=1)取消内联。
  • -gcflags=-l=2 和 -gcflags=-l=3 现在已经不使用了。和 -gcflags=-l=0 相比没有区别。
  • -gcflags=-l=4 减少非叶子函数和通过接口调用的函数的开销。 2

不确定语句的优化

一些函数虽然内联的开销很小,但由于太复杂它们仍不适合进行内联。这就是函数的不确定性,因为一些操作的语义在内联后很难去推导,如 recover、break。其他的操作,如 select 和 go 涉及运行时的协调,因此内联后引入的额外的开销不能抵消内联带来的收益。

不确定的语句也包括 for 和 range,这些语句不一定开销很大,但目前为止还没有对它们进行优化。

栈中函数优化

在过去,Go 编译器只对叶子函数进行内联 —— 只有那些不调用其他函数的函数才有资格。在上一段不确定的语句的探讨内容中,一次函数调用就会让这个函数失去内联的资格。

进入栈中进行内联,就像它的名字一样,能内联在函数调用栈中间的函数,不需要先让它下面的所有的函数都被标记为有资格内联的。栈中内联是 David Lazar 在 Go 1.9 中引入的,并在随后的版本中做了改进。 这篇文稿 深入探究了保留栈追踪行为和被深度内联后的代码路径里的 runtime.Callers 的难点。

在前面的例子中我们看到了栈中函数内联。内联后,func main() 包含了 func small() 的函数体和对 func large() 的一次调用,因此它被判定为非叶子函数。在过去,这会阻止它被继续内联,虽然它的联合开销小于内联预算。

栈中内联的最主要的应用案例就是减少贯穿函数调用栈的开销。考虑下面的例子:

package main

import (
    "fmt"
    "strconv"
)

type Rectangle struct {}

//go:noinline
func (r *Rectangle) Height() int {
    h, _ := strconv.ParseInt("7", 10, 0)
    return int(h)
}

func (r *Rectangle) Width() int {
    return 6
}

func (r *Rectangle) Area() int { return r.Height() * r.Width() }

func main() {
    var r Rectangle
    fmt.Println(r.Area())
}

在这个例子中, r.Area() 是个简单的函数,调用了两个函数。r.Width() 可以被内联,r.Height() 这里用 //go:noinline 指令标注了,不能被内联。 3

% go build -gcflags='-m=2' square.go                                                                                                          
# command-line-arguments
./square.go:12:6: cannot inline (*Rectangle).Height: marked go:noinline                                                                               
./square.go:17:6: can inline (*Rectangle).Width with cost 2 as: method(*Rectangle) func() int { return 6 }
./square.go:21:6: can inline (*Rectangle).Area with cost 67 as: method(*Rectangle) func() int { return r.Height() * r.Width() }                       
./square.go:21:61: inlining call to (*Rectangle).Width method(*Rectangle) func() int { return 6 }                                                     
./square.go:23:6: cannot inline main: function too complex: cost 150 exceeds budget 80                        
./square.go:25:20: inlining call to (*Rectangle).Area method(*Rectangle) func() int { return r.Height() * r.Width() }
./square.go:25:20: inlining call to (*Rectangle).Width method(*Rectangle) func() int { return 6 }

由于 r.Area() 中的乘法与调用它的开销相比并不大,因此内联它的表达式是纯收益,即使它的调用的下游 r.Height() 仍是没有内联资格的。

快速路径内联

关于栈中内联的效果最令人吃惊的例子是 2019 年 Carlo Alberto Ferraris 通过允许把 sync.Mutex.Lock() 的快速路径(非竞争的情况)内联到它的调用方来 提升它的性能 。在这个修改之前,sync.Mutex.Lock() 是个很大的函数,包含很多难以理解的条件,使得它没有资格被内联。即使锁可用时,调用者也要付出调用 sync.Mutex.Lock() 的代价。

Carlo 把 sync.Mutex.Lock() 分成了两个函数(他自己称为 外联(outlining))。外部的 sync.Mutex.Lock() 方法现在调用 sync/atomic.CompareAndSwapInt32() 且如果 CAS( 比较并交换(Compare and Swap))成功了之后立即返回给调用者。如果 CAS 失败,函数会走到 sync.Mutex.lockSlow() 慢速路径,需要对锁进行注册,暂停 goroutine。 4

% go build -gcflags='-m=2 -l=0' sync 2>&1 | grep '(*Mutex).Lock'
../go/src/sync/mutex.go:72:6: can inline (*Mutex).Lock with cost 69 as: method(*Mutex) func() { if "sync/atomic".CompareAndSwapInt32(&m.state, 0, mutexLocked) { if race.Enabled {  }; return  }; m.lockSlow() }

通过把函数分割成一个简单的不能再被分割的外部函数,和(如果没走到外部函数就走到的)一个处理慢速路径的复杂的内部函数,Carlo 组合了栈中函数内联和 编译器对基础操作的支持 ,减少了非竞争锁 14% 的开销。之后他在 sync.RWMutex.Unlock() 重复这个技巧,节省了另外 9% 的开销。

相关文章:

  1. Go 中的内联优化
  2. goroutine 的栈为什么会无限增长?
  3. 栈追踪和 errors 包
  4. 零值是什么,为什么它很有用?

  1. 不同发布版本中,在考虑该函数是否适合内联时,Go 编译器对同一函数的预算是不同的。 ↩
  2. 时刻记着编译器的作者警告过 “更高的内联等级(比 -l 更高)可能导致错误或不被支持” 。 Caveat emptor。 ↩
  3. 编译器有足够的能力来内联像 strconv.ParseInt 的复杂函数。作为一个实验,你可以尝试去掉 //go:noinline 注释,使用 -gcflags=-m=2 编译后观察。 ↩
  4. race.Enable 表达式是通过传递给 go 工具的 -race 参数控制的一个常量。对于普通编译,它的值是 false,此时编译器可以完全省略代码路径。 ↩

via: https://dave.cheney.net/2020/05/02/mid-stack-inlining-in-go

作者: Dave Cheney 选题: lujun9972 译者: lxbwolf 校对: wxy

本文由 LCTT 原创编译, Linux中国 荣誉推出

点击“了解更多”可访问文内链接

天将为大家带HTML的内联框架,以及网页进阶设计,在想对网页做出更进一步的完善时,我们可以使用JavaScript对网页设计出更多的样式以及使用响应式设计来设计出更加出众的网页外观。

一、HTML框架—iframe

1、Iframe是一种可以在网页内联其他网页的元素

2、Iframe语法为:<iframe arc = “URL”></iframe>(其中URL指向不同的网页)

3、Iframe以height和width属性来定义长度和宽度,示例:


运行结果:

4、在iframe中可以用frameborder属性来定义是否显示边框,设置属性为“0”的时候移除iframe的边框,示例:


运行结果:


二、HTML javaScript

1、JavaScript是面向Web的编程语言,获得了所有网页浏览器的支持,是目前使用最广泛的脚本编程语言之一,也是网页设计和Web应用必须掌握的基本工具。

2、在HTML中是用<script>标签来定义客户端脚本(JavaScript)。

3、JavaScript的常见用途是图像处理、表单验证和内容的动态更改。

4、JavaScript更改内容的示例:

<!DOCTYPE html>

<html>

<head>

<title>javascript示例</title>

</head>

<body>

<button type="button" onclick="myCat()">点击这里!</button>

<p id="eg1">看这里</p>

<script>

function myCat(){

document.getElementById("eg1").innerHTML ="hello!";

}

</script>

</body>

</html>


运行结果:

点击前

点击后


Javascrip可以修改样式,示例:

<!DOCTYPE html>

<html>

<body>

<h1>我的第一段 JavaScript</h1>

<p id="demo">JavaScript 可以更改 HTML 元素的样式。</p>

<script>

function myFunction() {

document.getElementById("demo").style.fontSize = "25px";

document.getElementById("demo").style.color = "red";

document.getElementById("demo").style.backgroundColor = "yellow";

}

</script>

<button type="button" onclick="myFunction()">点击我!</button>

</body>

</html>

运行结果:

点击前

点击后

三、HTML文件路径

1、文件路径描述了网站文件夹结构中某个文件的位置。

2、文件路径会在连接外部文件时被用到:

l 网页

l 图像

l 样式表

l JavaScript

3、绝对文件路径是指向一个因特网文件的完整URL,示例:

<img src="https://www.w3school.com.cn/images/picture.jpg" alt="flower">

运行结果:


4、相对路径指向了对于当前页面的文件。

四、HTML响应式设计

1、RWD指的是响应式Web设计(Responsive Web Design)。

2、RWD 能够以可变尺寸传递网页。

3、RWD 对于平板和移动设备是必需的。

创建响应式设计的一个方法,实在急来创建它,示例:

<!DOCTYPE html>

<html lang="en-US">

<head>

<style>

.city {

float: left;

margin: 5px;

padding: 15px;

width: 300px;

height: 300px;

border: 1px solid black;

}

</style>

</head>

<body>

<h1>Welcome to the New world</h1>

<h2>Resize this responsive page!</h2>

<br>

<div class="city">

<h2>London</h2>

<p>London is the capital city of England.</p>

<p>It is the most populous city in the United Kingdom,

with a metropolitan area of over 13 million inhabitants.</p>

</div>

<div class="city">

<h2>Paris</h2>

<p>Paris is the capital and most populous city of France.</p>

</div>

<div class="city">

<h2>Tokyo</h2>

<p>Tokyo is the capital of Japan, the center of the Greater Tokyo Area,

and the most populous metropolitan area in the world.</p>

</div>

</body>

</html>

运行结果:

4、另一个创建响应式设计的方法,是使用现成的 CSS 框架—Bootstrap。

5、Bootstrap 是最流行的开发响应式 web 的 HTML, CSS, 和 JS 框架。

6、Bootstrap 帮助您开发在任何尺寸都外观出众的站点:显示器、笔记本电脑、平板电脑或手机,示例:

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8">

<meta name="viewport" content="width=device-width, initial-scale=1">

<link rel="stylesheet"

href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">

</head>

<body>

<div class="container">

<div class="jumbotron">

<h1>Welcome to the New world</h1>

<p>Resize this responsive page!</p>

</div>

</div>

<div class="container">

<div class="row">

<div class="col-md-4">

<h2>London</h2>

<p>London is the capital city of England.</p>

<p>It is the most populous city in the United Kingdom,

with a metropolitan area of over 13 million inhabitants.</p>

</div>

<div class="col-md-4">

<h2>Paris</h2>

<p>Paris is the capital and most populous city of France.</p>

</div>

<div class="col-md-4">

<h2>Tokyo</h2>

<p>Tokyo is the capital of Japan, the center of the Greater Tokyo Area,

and the most populous metropolitan area in the world.</p>

</div>

</div>

</div>

</body>

</html>

运行结果:

学完这一节对网页的进阶设计内容,是不是觉得对网页设计有了更多的认识呢?

本文主要理理js模块化相关知识。
涉及到内联脚本、外联脚本、动态脚本、阻塞、
deferasyncCommonJSAMDCMDUMDES Module。顺带探究下Vite

内联脚本

假设你是一个前端新手,现在入门,那么我们创建一个html页面,需要新建一个index.html文件:

<!DOCTYPE html>
<html>
<head>
  <title>test</title>
</head>
<body>
  <p id="content">hello world</p>
</body>
</html>

如果需要在页面中执行javascript代码,我们就需要在 HTML 页面中插入 <script> 标签。

有2种插入方式:
1、放在
<head>
2、放在<body>

比如,点击hello world之后,在hello world后面加3个感叹号的功能,我们在head中加入script标签,并给hello world绑定点击事件:

<!DOCTYPE html>
<html>
<head>
  <title>test</title>
  <script>
    function myFunction() {
      document.getElementById('content').innerHTML = 'hello world!!!'
    }
  </script>
</head>

<body>
  <p id="content" onclick="myFunction()">hello world</p>
</body>
</html>

如果加在body中,一般放在body的最后面:

<!DOCTYPE html>
<html>
<head>
  <title>test</title>
</head>

<body>
  <p id="content" onclick="myFunction()">hello world</p>
  <script>
    function myFunction() {
      document.getElementById('content').innerHTML = 'hello world!!!'
    }
  </script>
</body>
</html>

简单的逻辑我们可以用这2种方式写,这种方式叫做内联脚本。

外联脚本

当逻辑复杂时,我们可以把上面的script标签中的代码抽取出来,比如在html的同级目录创建一个js文件夹,里面新建一个a.js的文件。

a.js中写上面script标签中的代码:

function myFunction() {
  document.getElementById('content').innerHTML = 'hello world!!!'
}

上面的script标签则可以改成:

<script src="./js/a.js"></script>

阻塞

上面的2种写法,浏览器在加载html时,遇到script标签,会停止解析html。
内联脚本会立刻执行;外联脚本会先下载再立刻执行。
等脚本执行完毕才会继续解析html。
(html解析到哪里,页面就能显示到哪里,用户也能看到哪里)

比如下面的代码:

<p>...content before script...</p>

<script src="./js/a.js"></script>

<p>...content after script...</p>

解析到第一个p标签,我们能看到...content before script...显示在了页面中,然后浏览器遇到script标签,会停止解析html,而去下载a.js并执行,执行完a.js才会继续解析html,然后页面中才会出现...content after script...

我们可以通过Chrome的Developer Tools分析一下index.html加载的时间线:

这会导致2个问题:
1、脚本无法访问它下面的dom;
2、如果页面顶部有个笨重的脚本,在它执行完之前,用户都看不到完整的页面。

对于问题2,我们可以把脚本放在页面底部,这样它可以访问到上面的dom,且不会阻塞页面的显示:

<body>
  ...all content is above the script...

  <script src="./js/a.js"></script>
</body>

但这不是最好的办法,我们接着往下看。

defer

我们给script标签加defer属性,就像下面这样:

<p>...content before script...</p>

<script defer src="./js/a.js"></script>

<p>...content after script...</p>

defer 特性告诉浏览器不要等待脚本。于是,浏览器将继续解析html,脚本会并行下载,然后等 DOM 构建完成后,脚本才会执行。

这样script标签不再阻塞html的解析。

这时再看时间线:

需要注意的是,具有 defer 特性的脚本保持其相对顺序。

比如:

<script defer src="./js/a.js"></script>
<script defer src="./js/b.js"></script>

上面的2个脚本会并行下载,但是不论哪个先下载完成,都是先执行a.js,a.js执行完才会执行b.js。
这时,如果b.js依赖a.js,这种写法将很有用。

另外需要注意的是,defer 特性仅适用于外联脚本,即如果 script标签没有 src属性,则会忽略 defer 特性。

async

我们可以给script标签加async属性,就像下面这样:

<script async src="./js/a.js"></script>

这会告诉浏览器,该脚本完全独立。
独立的意思是,DOM 和其他脚本不会等待它,它也不会等待其它东西。async 脚本就是一个会在加载完成时立即执行的完全独立的脚本。

这时再看时间线:

可以看到,虽然下载a.js不阻塞html的解析,但是执行a.js会阻塞。

还需要注意多个async时的执行顺序,比如下面这段代码:

<p>...content before script...</p>

<script async src="./js/a.js"></script>
<script async src="./js/b.js"></script>

<p>...content after script...</p>

两个p标签的内容会立刻显示出来,a.js和b.js则并行下载,且下载成功后立刻执行,所以多个async时的执行顺序是谁先下载成功谁先执行。
一些比较独立的脚本,比如性能监控,就很适合用这种方式加载。

另外,和defer一样,async 特性也仅适用于外联脚本。

动态脚本

我们可以动态地创建一个script标签并append到文档中。

let script = document.createElement('script')
script.src = '/js/a.js'
document.body.append(script)

append后脚本就会立刻开始加载,表现默认和加了async属性一致。
我们可以显示的设置
script.async = false来改变这个默认行为,那么这时表现就和加了defer属性一致。

上面的这些写法,当script标签变多时,容易导致全局作用域污染,还要维护书写顺序,要解决这个问题,需要一种将 JavaScript 程序拆分为可按需导入的单独模块的机制,即js模块化,我们接着往下看。

CommonJS

很长一段时间 JavaScript 没有模块化的概念,直到 Node.js 的诞生,把 JavaScript 带到服务端,这时,CommonJS诞生了。

CommonJS定义了三个全局变量:

require,exports,module

require 读入并执行一个 js 文件,然后返回其 exports 对象;
exports 对外暴露模块的接口,可以是任何类型,指向 module.exports;
module 是当前模块,exports 是 module 上的一个属性。

Node.js 使用了CommonJS规范。

比如:

// a.js
let name = 'Lily'
export.name = name

// b.js
let a = require('a.js')
console.log(a.name) // Lily

由于CommonJS不适合浏览器端,于是出现了AMD和CMD规范。

AMD

AMD(Asynchronous Module Definition) 是 RequireJS 在推广过程中对模块定义的规范化产出。

基本思想是,通过 define 方法,将代码定义为模块。当这个模块被 require 时,开始加载依赖的模块,当所有依赖的模块加载完成后,开始执行回调函数,返回该模块导出的值。

使用时,需要先引入require.js:

<script src="require.js"></script>
<script src="a.js"></script>

然后可以这样写:

// a.js
define(function() {
    let name = 'Lily'
    return {
        name
    }
})
// b.js
define(['a.js'], function(a) {
    let name = 'Bob'
    console.log(a.name) // Lily
    return {
        name
    }
})

CMD

CMD(Common Module Definition) 是 Sea.js 在推广过程中对模块定义的规范化产出。

使用时,需要先引入sea.js:

<script src="sea.js"></script>
<script src="a.js"></script>

然后可以这样写:

// a.js
define(function(require, exports, module) {
    var name = 'Lily'
    exports.name = name
})

// b.js
define(function(require, exports, module) {
    var name = 'Bob'
    var a = require('a.js')
    console.log(a.name) // 'Lily'
    exports.name = name
})

UMD

UMD (Universal Module Definition) 目的是提供一个前后端跨平台的解决方案(兼容全局变量、AMD、CMD和CommonJS)。

实现很简单,判断不同的环境,然后以不同的方式导出模块:

(function (root, factory) {
    if (typeof define === 'function' && (define.amd || define.cmd)) {
        // AMD、CMD
        define([], factory);
    } else if (typeof module !== 'undefined' && typeof exports === 'object') {
        // Node、CommonJS
        module.exports = factory();
    } else {
        // 浏览器全局变量
        root.moduleName = factory();
  }
}(this, function () {
    // 只需要返回一个值作为模块的export
    // 这里我们返回了一个空对象
    // 你也可以返回一个函数
    return {};
}));

ES Module

AMD 和 CMD 是社区的开发者们制定的模块加载方案,并不是语言层面的标准。从 ES6 开始,在语言标准的层面上,实现了模块化功能,而且实现得相当简单,完全可以取代上文的规范,成为浏览器和服务器通用的模块解决方案。

ES6 的模块自动采用严格模式。模块功能主要由两个命令构成:export和import。

export命令用于规定模块的对外接口;
import命令用于输入其他模块提供的功能。

比如上面的代码,我们可以这样写:

// a.js
const name = 'Lily'

export {
  name
}

// 等价于
export const name = 'Lily'

// b.js
import { name } from 'a.js'
console.log(name) // Lily

// b.js
import * as a from 'a.js'
console.log(a.name) // Lily

此外,还可以用export default默认导出的写法:

// a.js
const name = 'Lily'

export default {
  name
}

// b.js
import a from 'a.js'
console.log(a.name) // Lily

如果只想运行a.js,可以只import:

// b.js
import 'a.js'

我们可以给script标签加type=module让浏览器以 ES Module 的方式加载脚本:

<script type="module" src="./js/b.js"></script>

这时,script标签会默认有defer属性(也可以设置成async),支持内联和外联脚本。

这时我们运行打开index.html,会发现浏览器报错了:

这是因为 type=module 的 script 标签加强了安全策略,浏览器加载不同域的脚本资源时,如果服务器未返回有效的 Allow-Origin 相关 CORS 头,会禁止加载改脚本。而这里启动的index.html是一个本地文件(地址是file://路径),将会遇到 CORS 错误,需要通过一个服务器来启动 HTML 文件。

Vite

在浏览器支持 ES Module 之前,我们用工具实现JavaScript模块化的开发,比如webpack、Rollup 和 Parcel 。但是当项目越来越大后,本地热更新越来越慢,而 Vite 旨在利用ESM解决上述问题。

Vite使用简单,可以去官网(https://cn.vitejs.dev/)看看。

总结

老的规范了解即可,未来是ES Module的,用Vite可以极大的提升开发时的体验,生产环境用Rollup打包。