文中,我要論述內聯的限制以及葉子內聯與棧中內聯mid-stack inlining的對比。
上一篇文章 中我論述了 葉子內聯(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 參數控制內聯預算的等級。下面是可使用的值:
不確定語句的優化
一些函數雖然內聯的開銷很小,但由于太復雜它們仍不適合進行內聯。這就是函數的不確定性,因為一些操作的語義在內聯后很難去推導,如 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% 的開銷。
via: https://dave.cheney.net/2020/05/02/mid-stack-inlining-in-go
作者: Dave Cheney 選題: lujun9972 譯者: lxbwolf 校對: wxy
本文由 LCTT 原創編譯, Linux中國 榮譽推出
天將為大家帶HTML的內聯框架,以及網頁進階設計,在想對網頁做出更進一步的完善時,我們可以使用JavaScript對網頁設計出更多的樣式以及使用響應式設計來設計出更加出眾的網頁外觀。
1、Iframe是一種可以在網頁內聯其他網頁的元素
2、Iframe語法為:<iframe arc = “URL”></iframe>(其中URL指向不同的網頁)
3、Iframe以height和width屬性來定義長度和寬度,示例:
運行結果:
4、在iframe中可以用frameborder屬性來定義是否顯示邊框,設置屬性為“0”的時候移除iframe的邊框,示例:
運行結果:
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>
運行結果:
點擊前
點擊后
1、文件路徑描述了網站文件夾結構中某個文件的位置。
2、文件路徑會在連接外部文件時被用到:
l 網頁
l 圖像
l 樣式表
l JavaScript
3、絕對文件路徑是指向一個因特網文件的完整URL,示例:
<img src="https://www.w3school.com.cn/images/picture.jpg" alt="flower">
運行結果:
4、相對路徑指向了對于當前頁面的文件。
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模塊化相關知識。
涉及到內聯腳本、外聯腳本、動態腳本、阻塞、defer、async、CommonJS、AMD、CMD、UMD、ES 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>
但這不是最好的辦法,我們接著往下看。
我們給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 特性。
我們可以給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模塊化,我們接著往下看。
很長一段時間 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(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(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 (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 {};
}));
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 文件。
在瀏覽器支持 ES Module 之前,我們用工具實現JavaScript模塊化的開發,比如webpack、Rollup 和 Parcel 。但是當項目越來越大后,本地熱更新越來越慢,而 Vite 旨在利用ESM解決上述問題。
Vite使用簡單,可以去官網(https://cn.vitejs.dev/)看看。
老的規范了解即可,未來是ES Module的,用Vite可以極大的提升開發時的體驗,生產環境用Rollup打包。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。