分类 软件开发 下的文章

今天我在 libcurl 内部又做了一个小改动,使其做更少的 malloc。这一次,泛型链表函数被转换成更少的 malloc (这才是链表函数应有的方式,真的)。

研究 malloc

几周前我开始研究内存分配。这很容易,因为多年前我们 curl 中就已经有内存调试和日志记录系统了。使用 curl 的调试版本,并在我的构建目录中运行此脚本:

#!/bin/sh
export CURL_MEMDEBUG=$HOME/tmp/curlmem.log
./src/curl http://localhost
./tests/memanalyze.pl -v $HOME/tmp/curlmem.log

对于 curl 7.53.1,这大约有 115 次内存分配。这算多还是少?

内存日志非常基础。为了让你有所了解,这是一个示例片段:

MEM getinfo.c:70 free((nil))
MEM getinfo.c:73 free((nil))
MEM url.c:294 free((nil))
MEM url.c:297 strdup(0x559e7150d616) (24) = 0x559e73760f98
MEM url.c:294 free((nil))
MEM url.c:297 strdup(0x559e7150d62e) (22) = 0x559e73760fc8
MEM multi.c:302 calloc(1,480) = 0x559e73760ff8
MEM hash.c:75 malloc(224) = 0x559e737611f8
MEM hash.c:75 malloc(29152) = 0x559e737a2bc8
MEM hash.c:75 malloc(3104) = 0x559e737a9dc8

检查日志

然后,我对日志进行了更深入的研究,我意识到在相同的代码行做了许多小内存分配。我们显然有一些相当愚蠢的代码模式,我们分配一个结构体,然后将该结构添加到链表或哈希,然后该代码随后再添加另一个小结构体,如此这般,而且经常在循环中执行。(我在这里说的是我们,不是为了责怪某个人,当然大部分的责任是我自己……)

这两种分配操作将总是成对地出现,并被同时释放。我决定解决这些问题。做非常小的(小于 32 字节)的分配也是浪费的,因为非常多的数据将被用于(在 malloc 系统内)跟踪那个微小的内存区域。更不用说堆碎片了。

因此,将该哈希和链表代码修复为不使用 malloc 是快速且简单的方法,对于最简单的 “curl http://localhost” 传输,它可以消除 20% 以上的 malloc。

此时,我根据大小对所有的内存分配操作进行排序,并检查所有最小的分配操作。一个突出的部分是在 curl_multi_wait() 中,它是一个典型的在 curl 传输主循环中被反复调用的函数。对于大多数典型情况,我将其转换为使用堆栈。在大量重复的调用函数中避免 malloc 是一件好事。

重新计数

现在,如上面的脚本所示,同样的 curl localhost 命令从 curl 7.53.1 的 115 次分配操作下降到 80 个分配操作,而没有牺牲任何东西。轻松地有 26% 的改善。一点也不差!

由于我修改了 curl_multi_wait(),我也想看看它实际上是如何改进一些稍微更高级一些的传输。我使用了 multi-double.c 示例代码,添加了初始化内存记录的调用,让它使用 curl_multi_wait(),并且并行下载了这两个 URL:

http://www.example.com/
http://localhost/512M

第二个文件是 512 兆字节的零,第一个文件是一个 600 字节的公共 html 页面。这是 count-malloc.c 代码

首先,我使用 7.53.1 来测试上面的例子,并使用 memanalyze 脚本检查:

Mallocs: 33901
Reallocs: 5
Callocs: 24
Strdups: 31
Wcsdups: 0
Frees: 33956
Allocations: 33961
Maximum allocated: 160385

好了,所以它总共使用了 160KB 的内存,分配操作次数超过 33900 次。而它下载超过 512 兆字节的数据,所以它每 15KB 数据有一次 malloc。是好是坏?

回到 git master,现在是 7.54.1-DEV 的版本 - 因为我们不太确定当我们发布下一个版本时会变成哪个版本号。它可能是 7.54.1 或 7.55.0,它还尚未确定。我离题了,我再次运行相同修改的 multi-double.c 示例,再次对内存日志运行 memanalyze,报告来了:

Mallocs: 69
Reallocs: 5
Callocs: 24
Strdups: 31
Wcsdups: 0
Frees: 124
Allocations: 129
Maximum allocated: 153247

我不敢置信地反复看了两遍。发生什么了吗?为了仔细检查,我最好再运行一次。无论我运行多少次,结果还是一样的。

33961 vs 129

在典型的传输中 curl_multi_wait() 被调用了很多次,并且在传输过程中至少要正常进行一次内存分配操作,因此删除那个单一的微小分配操作对计数器有非常大的影响。正常的传输也会做一些将数据移入或移出链表和散列操作,但是它们现在也大都是无 malloc 的。简单地说:剩余的分配操作不会在传输循环中执行,所以它们的重要性不大。

以前的 curl 是当前示例分配操作数量的 263 倍。换句话说:新的是旧的分配操作数量的 0.37% 。

另外还有一点好处,新的内存分配量更少,总共减少了 7KB(4.3%)。

malloc 重要吗?

在几个 G 内存的时代里,在传输中有几个 malloc 真的对于普通人有显著的区别吗?对 512MB 数据进行的 33832 个额外的 malloc 有什么影响?

为了衡量这些变化的影响,我决定比较 localhost 的 HTTP 传输,看看是否可以看到任何速度差异。localhost 对于这个测试是很好的,因为没有网络速度限制,更快的 curl 下载也越快。服务器端也会相同的快/慢,因为我将使用相同的测试集进行这两个测试。

我相同方式构建了 curl 7.53.1 和 curl 7.54.1-DEV,并运行这个命令:

curl http://localhost/80GB -o /dev/null

下载的 80GB 的数据会尽可能快地写到空设备中。

我获得的确切数字可能不是很有用,因为它将取决于机器中的 CPU、使用的 HTTP 服务器、构建 curl 时的优化级别等,但是相对数字仍然应该是高度相关的。新代码对决旧代码!

7.54.1-DEV 反复地表现出更快 30%!我的早期版本是 2200MB/秒增加到当前版本的超过 2900 MB/秒。

这里的要点当然不是说它很容易在我的机器上使用单一内核以超过 20GB/秒的速度来进行 HTTP 传输,因为实际上很少有用户可以通过 curl 做到这样快速的传输。关键在于 curl 现在每个字节的传输使用更少的 CPU,这将使更多的 CPU 转移到系统的其余部分来执行任何需要做的事情。或者如果设备是便携式设备,那么可以省电。

关于 malloc 的成本:512MB 测试中,我使用旧代码发生了 33832 次或更多的分配。旧代码以大约 2200MB/秒的速率进行 HTTP 传输。这等于每秒 145827 次 malloc - 现在它们被消除了!600 MB/秒的改进意味着每秒钟 curl 中每个减少的 malloc 操作能额外换来多传输 4300 字节。

去掉这些 malloc 难吗?

一点也不难,非常简单。然而,有趣的是,在这个旧项目中,仍然有这样的改进空间。我有这个想法已经好几年了,我很高兴我终于花点时间来实现。感谢我们的测试套件,我可以有相当大的信心做这个“激烈的”内部变化,而不会引入太可怕的回归问题。由于我们的 API 很好地隐藏了内部,所以这种变化可以完全不改变任何旧的或新的应用程序……

(是的,我还没在版本中发布该变更,所以这还有风险,我有点后悔我的“这很容易”的声明……)

注意数字

curl 的 git 仓库从 7.53.1 到今天已经有 213 个提交。即使我没有别的想法,可能还会有一次或多次的提交,而不仅仅是内存分配对性能的影响。

还有吗?

还有其他类似的情况么?

也许。我们不会做很多性能测量或比较,所以谁知道呢,我们也许会做更多的愚蠢事情,我们可以收手并做得更好。有一个事情是我一直想做,但是从来没有做,就是添加所使用的内存/malloc 和 curl 执行速度的每日“监视” ,以便更好地跟踪我们在这些方面不知不觉的回归问题。

补遗,4/23

(关于我在 hacker news、Reddit 和其它地方读到的关于这篇文章的评论)

有些人让我再次运行那个 80GB 的下载,给出时间。我运行了三次新代码和旧代码,其运行“中值”如下:

旧代码:

real    0m36.705s
user    0m20.176s
sys     0m16.072s

新代码:

real    0m29.032s
user    0m12.196s
sys     0m12.820s

承载这个 80GB 文件的服务器是标准的 Apache 2.4.25,文件存储在 SSD 上,我的机器的 CPU 是 i7 3770K 3.50GHz 。

有些人也提到 alloca() 作为该补丁之一也是个解决方案,但是 alloca() 移植性不够,只能作为一个孤立的解决方案,这意味着如果我们要使用它的话,需要写一堆丑陋的 #ifdef


via: https://daniel.haxx.se/blog/2017/04/22/fewer-mallocs-in-curl/

作者:DANIEL STENBERG 译者:geekpi 校对:wxy

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

我们来解释函数式编程的什么,它的优点是哪些,并且给出一些函数式编程的学习资源。

 title=

这要看您问的是谁, 函数式编程 functional programming (FP)要么是一种理念先进的、应该广泛传播的程序设计方法;要么是一种偏学术性的、实际用途不多的编程方式。在这篇文章中我将讲解函数式编程,探究其优点,并推荐学习函数式编程的资源。

语法入门

本文的代码示例使用的是 Haskell 编程语言。在这篇文章中你只需要了解的基本函数语法:

even :: Int -> Bool
even = ...    -- 具体的实现放在这里

上述示例定义了含有一个参数的函数 even ,第一行是 类型声明,具体来说就是 even 函数接受一个 Int 类型的参数,返回一个 Bool 类型的值,其实现跟在后面,由一个或多个等式组成。在这里我们将忽略具体实现方法(名称和类型已经足够了):

map :: (a -> b) -> [a] -> [b]
map = ...

这个示例,map 是一个有两个参数的函数:

  1. (a -> b) :将 a 转换成 b 的函数
  2. [a]:一个 a 的列表,并返回一个 b 的列表。(LCTT 译注: 将函数作用到 [a] (List 序列对应于其它语言的数组)的每一个元素上,将每次所得结果放到另一个 [b] ,最后返回这个结果 [b]。)

同样我们不去关心要如何实现,我们只感兴趣它的定义类型。ab 是任何一种的的 类型变量 type variable 。就像上一个示例中, aInt 类型, bBool 类型:

map even [1,2,3]

这个是一个 Bool 类型的序列:

[False,True,False]

如果你看到你不理解的其他语法,不要惊慌;对语法的充分理解不是必要的。

函数式编程的误区

我们先来解释一下常见的误区:

  • 函数式编程不是命令行编程或者面向对象编程的竞争对手或对立面,这并不是非此即彼的。
  • 函数式编程不仅仅用在学术领域。这是真的,在函数式编程的历史中,如像 Haskell 和 OCaml 语言是最流行的研究语言。但是今天许多公司使用函数式编程来用于大型的系统、小型专业程序,以及种种不同场合。甚至还有一个面向函数式编程的商业用户[33的年度会议;以前的那些程序让我们了解了函数式编程在工业中的用途,以及谁在使用它。
  • 函数式编程与 monad 无关 ,也不是任何其他特殊的抽象。在这篇文章里面 monad 只是一个抽象的规定。有些是 monad,有些不是。
  • 函数式编程不是特别难学的。某些语言可能与您已经知道的语法或求值语义不同,但这些差异是浅显的。函数式编程中有大量的概念,但其他语言也是如此。

什么是函数式编程?

核心是函数式编程是只使用纯粹的数学函数编程,函数的结果仅取决于参数,而没有副作用,就像 I/O 或者状态转换这样。程序是通过 组合函数 function composition 的方法构建的:

(.) :: (b -> c) -> (a -> b) -> (a -> c)
(g . f) x = g (f x)

这个 中缀 infix 函数 (.) 表示的是二个函数组合成一个,将 g 作用到 f 上。我们将在下一个示例中看到它的使用。作为比较,我们看看在 Python 中同样的函数:

def compose(g, f):
  return lambda x: g(f(x))

函数式编程的优点在于:由于函数是确定的、没有副作用的,所以可以用结果替换函数,这种替代等价于使用使 等式推理 equational reasoning 。每个程序员都有使用自己代码和别人代码的理由,而等式推理就是解决这样问题不错的工具。来看一个示例。等你遇到这个问题:

map even . map (+1)

这段代码是做什么的?可以简化吗?通过等式推理,可以通过一系列替换来分析代码:

map even . map (+1)
map (even . (+1))         -- 来自 'map' 的定义
map (\x -> even (x + 1))  -- lambda 抽象
map odd                   -- 来自 'even' 的定义

我们可以使用等式推理来理解程序并优化可读性。Haskell 编译器使用等式推理进行多种程序优化。没有纯函数,等式推理是不可能的,或者需要程序员付出更多的努力。

函数式编程语言

你需要一种编程语言来做函数式编程吗?

在没有 高阶函数 higher-order function (传递函数作为参数和返回函数的能力)、lambdas (匿名函数)和 泛型 generics 的语言中进行有意义的函数式编程是困难的。 大多数现代语言都有这些,但在不同语言中支持函数式编程方面存在差异。 具有最佳支持的语言称为 函数式编程语言 functional programming language 。 这些包括静态类型的 HaskellOCamlF#Scala ,以及动态类型的 ErlangClojure

即使是在函数式语言里,可以在多大程度上利用函数编程有很大差异。有一个 类型系统 type system 会有很大的帮助,特别是它支持 类型推断 type inference 的话(这样你就不用总是必须键入类型)。这篇文章中没有详细介绍这部分,但足以说明,并非所有的类型系统都是平等的。

与所有语言一样,不同的函数的语言强调不同的概念、技术或用例。选择语言时,考虑它支持函数式编程的程度以及是否适合您的用例很重要。如果您使用某些非 FP 语言,你仍然会受益于在该语言支持的范围内的函数式编程。

不要打开陷阱之门

回想一下,函数的结果只取决于它的输入。但是,几乎所有的编程语言都有破坏这一原则的“功能”。空值、 实例类型 type case instanceof)、类型转换、异常、 边际效用 side-effect ,以及无尽循环的可能性都是陷阱,它打破等式推理,并削弱程序员对程序行为正确性的理解能力。(所有语言里面,没有任何陷阱的语言包括 Agda、Idris 和 Coq。)

幸运的是,作为程序员,我们可以选择避免这些陷阱,如果我们受到严格的规范,我们可以假装陷阱不存在。 这个方法叫做 轻率推理 fast and loose reasoning 。它不需要任何条件,几乎任何程序都可以在不使用陷阱的情况下进行编写,并且通过避免这些可以而获得等式推理、可组合性和可重用性。

让我们详细讨论一下。 这个陷阱破坏了等式推理,因为异常终止的可能性没有反映在类型中。(你可以庆幸文档中甚至没有提到能抛出的异常)。但是没有理由我们没有一个可以包含所有故障模式的返回类型。

避开陷阱是语言特征中出现很大差异的领域。为避免例外, 代数数据类型 algebraic data type 可用于模型错误的条件下,就像:

-- new data type for results of computations that can fail
--
data Result e a = Error e | Success a

-- new data type for three kinds of arithmetic errors
--
data ArithError = DivByZero | Overflow | Underflow

-- integer division, accounting for divide-by-zero
--
safeDiv :: Int -> Int -> Result ArithError Int
safeDiv x y =
  if y == 0
    then Error DivByZero
    else Success (div x y)

在这个例子中的权衡你现在必须使用 Result ArithError Int 类型,而不是以前的 Int 类型,但这也是解决这个问题的一种方式。你不再需要处理异常,而能够使用轻率推理 ,总体来说这是一个胜利。

自由定理

大多数现代静态类型语言具有 范型 generics (也称为 参数多态性 parametric polymorphism ),其中函数是通过一个或多个抽象类型定义的。 例如,看看这个 List(序列)函数:

f :: [a] -> [a]
f = ...

Java 中的相同函数如下所示:

static <A> List<A> f(List<A> xs) { ... }

该编译的程序证明了这个函数适用于类型 a任意选择。考虑到这一点,采用轻率推理的方法,你能够弄清楚该函数的作用吗?知道类型有什么帮助?

在这种情况下,该类型并不能告诉我们函数的功能(它可以逆转序列、删除第一个元素,或许多其它的操作),但它确实告诉了我们很多信息。只是从该类型,我们可以推演出该函数的定理:

  • 定理 1 :输出中的每个元素也出现于输入中;不可能在输入的序列 a 中添加值,因为你不知道 a 是什么,也不知道怎么构造一个。
  • 定理 2 :如果你映射某个函数到列表上,然后对其应用 f,其等同于对映射应用 f

定理 1 帮助我们了解代码的作用,定理 2 对于程序优化提供了帮助。我们从类型中学到了这一切!其结果,即从类型中获取有用的定理的能力,称之为 参数化 parametricity 。因此,类型是函数行为的部分(有时是完整的)规范,也是一种机器检查机制。

现在你可以利用参数化了。你可以从 map(.) 的类型或者下面的这些函数中发现什么呢?

  • foo :: a -> (a, a)
  • bar :: a -> a -> a
  • baz :: b -> a -> a

学习功能编程的资源

也许你已经相信函数式编程是编写软件不错的方式,你想知道如何开始?有几种学习功能编程的方法;这里有一些我推荐(我承认,我对 Haskell 偏爱):

  • UPenn 的 CIS 194: 介绍 Haskell 是函数式编程概念和 Haskell 实际开发的不错选择。有课程材料,但是没有讲座(您可以用几年前 Brisbane 函数式编程小组的 CIS 194 系列讲座
  • 不错的入门书籍有 《Scala 的函数式编程》 、 《Haskell 函数式编程思想》 , 和 《Haskell 编程原理》。
  • Data61 FP 课程 (即 NICTA 课程)通过 类型驱动开发 type-driven development 来教授基础的抽象概念和数据结构。这是十分困难,但收获也是丰富的,其起源于培训会,如果你有一名愿意引导你函数式编程的程序员,你可以尝试。
  • 在你的工作学习中使用函数式编程书写代码,写一些纯函数(避免不确定性和异常的出现),使用高阶函数和递归而不是循环,利用参数化来提高可读性和重用性。许多人从体验和实验各种语言的美妙之处,开始走上了函数式编程之旅。
  • 加入到你的地区中的一些函数式编程小组或者学习小组中,或者创建一个,也可以是参加一些函数式编程的会议(新的会议总是不断的出现)。

总结

在本文中,我讨论了函数式编程是什么以及不是什么,并了解到了函数式编程的优势,包括等式推理和参数化。我们了解到在大多数编程语言中都有一些函数式编程功能,但是语言的选择会影响受益的程度,而 Haskell 是函数式编程中语言最受欢迎的语言。我也推荐了一些学习函数式编程的资源。

函数式编程是一个丰富的领域,还有许多更深入(更神秘)的主题正在等待探索。我没有提到那些具有实际意义的事情,比如:

  • lenses 和 prisms (是一流的设置和获取值的方式;非常适合使用嵌套数据);
  • 定理证明(当你可以证明你的代码正确时,为什么还要测试你的代码?);
  • 延迟评估(让您处理潜在的无数的数据结构);
  • 分类理论(函数式编程中许多美丽实用的抽象的起源);

我希望你喜欢这个函数式编程的介绍,并且启发你走上这个有趣和实用的软件开发之路。

本文根据 CC BY 4.0 许可证发布。

(题图: opensource.com)


作者简介:

红帽软件工程师。对函数式编程,分类理论,数学感兴趣。Crazy about jalapeños.


via: https://opensource.com/article/17/4/introduction-functional-programming

作者:Fraser Tweedale 译者:MonkeyDEcho 校对:wxy

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

如果你想使用 Headless Chrome 进行自动化测试,那么就往下!这篇文章将让你完全使用 Karma 作为 运行器 runner ,并且使用 Mocha+Chai 来编撰测试。

这些东西是什么?

Karma、Mocha、Chai、Headless Chrome,哦,我的天哪!

Karma 是一个测试工具,可以和所有最流行的测试框架(JasmineMochaQUnit)配合使用。

Chai 是一个断言库,可以与 Node 和浏览器一起使用。这里我们需要后者。

Headless Chrome 是一种在没有浏览器用户界面的无需显示环境中运行 Chrome 浏览器的方法。使用 Headless Chrome(而不是直接在 Node 中测试) 的一个好处是 JavaScript 测试将在与你的网站用户相同的环境中执行。Headless Chrome 为你提供了真正的浏览器环境,却没有运行完整版本的 Chrome 一样的内存开销。

设置

安装

使用 yarn 安装 Karma、相关插件和测试用例:

yarn add --dev karma karma-chrome-launcher karma-mocha karma-chai
yarn add --dev mocha chai

或者使用 npm

npm i --save-dev karma karma-chrome-launcher karma-mocha karma-chai
npm i --save-dev mocha chai

在这篇文章中我使用 MochaChai,但是你也可以选择自己最喜欢的在浏览器中工作的断言库。

配置 Karma

创建一个使用 ChromeHeadless 启动器的 karma.config.js 文件。

karma.conf.js

module.exports = function(config) {
  config.set({
    frameworks: ['mocha', 'chai'],
    files: ['test/**/*.js'],
    reporters: ['progress'],
    port: 9876,  // karma web server port
    colors: true,
    logLevel: config.LOG_INFO,
    browsers: ['ChromeHeadless'],
    autoWatch: false,
    // singleRun: false, // Karma captures browsers, runs the tests and exits
    concurrency: Infinity
  })
}
注意: 运行 ./node_modules/karma/bin/karma init karma.conf.js 生成 Karma 的配置文件。

写一个测试

/test/test.js 中写一个测试:

/test/test.js

describe('Array', () => {
  describe('#indexOf()', () => {
    it('should return -1 when the value is not present', () => {
      assert.equal(-1, [1,2,3].indexOf(4));
    });
  });
});

运行你的测试

在我们设置好用于运行 Karma 的 package.json 中添加一个测试脚本。

package.json

"scripts": {
  "test": "karma start --single-run --browsers ChromeHeadless karma.conf.js"
}

当你运行你的测试(yarn test)时,Headless Chrome 会启动并将运行结果输出到终端:

创建你自己的 Headless Chrome 启动器

ChromeHeadless 启动器非常棒,因为它可以在 Headless Chrome 上进行测试。它包含了适合你的 Chrome 标志,并在端口 9222 上启动 Chrome 的远程调试版本。

但是,有时你可能希望将自定义的标志传递给 Chrome 或更改启动器使用的远程调试端口。要做到这一点,可以通过创建一个 customLaunchers 字段来扩展基础的 ChromeHeadless 启动器:

karma.conf.js

module.exports = function(config) {
  ...

  config.set({
    browsers: ['Chrome', 'ChromeHeadless', 'MyHeadlessChrome'],

    customLaunchers: {
      MyHeadlessChrome: {
        base: 'ChromeHeadless',
        flags: ['--disable-translate', '--disable-extensions', '--remote-debugging-port=9223']
      }
    },
  }
};

完全在 Travis CI 上运行它

在 Headless Chrome 中配置 Karma 运行测试是很困难的。而在 Travis 中持续集成就只有几种!

要在 Travis 中运行测试,请使用 dist: trusty 并安装稳定版 Chrome 插件:

.travis.yml

language: node_js
node_js:
  - "7"
dist: trusty # needs Ubuntu Trusty
sudo: false  # no need for virtualization.
addons:
  chrome: stable # have Travis install chrome stable.
cache:
  yarn: true
  directories:
    - node_modules
install:
  - yarn
script:
  - yarn test

作者简介

Eric Bidelman 谷歌工程师,Lighthouse 开发,Web 和 Web 组件开发,Chrome 开发


via: https://developers.google.com/web/updates/2017/06/headless-karma-mocha-chai

作者:Eric Bidelman 译者:firmianay 校对:wxy

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

通过使用 GitHub 的 拉取请求 Pull Request 正确地进行代码审核,把时间更多的花在构建上,而在修复上少用点时间。

Measure

如果你不是每天编写代码,你可能不知道软件开发人员日常面临的一些问题。

  • 代码中的安全漏洞
  • 导致应用程序崩溃的代码
  • 被称作 “技术债务” 和之后需要重写的代码
  • 在某处你所不知道地方的代码已经被重写

代码审查 Code review 可以允许其他的人或工具来检查代码,帮助我们改善所编写的软件。这种审查(也称为 同行评审 peer review )能够通过自动化代码分析或者测试覆盖工具来进行,是软件开发过程中两个重要的部分,它能够节省数小时的手工工作。同行评审是开发人员审查彼此工作的一个过程。在软件开发的过程中,速度和紧迫性是两个经常提及的问题。如果你没有尽快的发布,你的竞争对手可能会率先发布新功能。如果你不能够经常发布新的版本,你的用户可能会怀疑您是否仍然关心改进你的应用程序。

权衡时间:代码审查与缺陷修复

如果有人能够以最小争议的方式汇集多种类型的代码审查,那么随着时间的推移,该软件的质量将会得到改善。如果认为引入新的工具或流程最先导致的不是延迟,那未免太天真了。但是代价更高昂的是:修复生产环境中的错误花费的时间,或者在放到生产环境之前改进软件所花费的时间。即使新工具延迟了新功能的发布和得到客户欣赏的时间,但随着软件开发人员提高自己的技能,该延迟会缩短,软件开发周期将会回升到以前的水平,而同时缺陷将会减少。

通过代码审查实现提升代码质量目标的关键之一就是使用一个足够灵活的平台,允许软件开发人员快速编写代码,置入他们熟悉的工具,并对彼此进行同行评审。 GitHub 就是这样的平台的一个很好的例子。然而,只是把你的代码放在 GitHub 上并不会魔术般地使代码审查发生;你必须使用 拉取请求 Pull Request 来开始这个美妙的旅程。

拉取请求:关于代码的现场讨论

拉取请求 Pull Request 是 Github 上的一个工具,允许软件开发人员讨论并提出对项目的主要代码库的更改,这些更改稍后可以部署给所有用户看到。这个功能创建于 2008 年 2 月,其目的是在接受(合并)之前,对某人的建议进行更改,然后在部署到生产环境中,供最终用户看到这种变化。

拉取请求开始是以一种松散的方式让你为某人的项目提供更改,但是它们已经演变成:

  • 关于你想要合并的代码的现场讨论
  • 提升了所更改内容的可视功能
  • 整合了你最喜爱的工具
  • 作为受保护的分支工作流程的一部分可能需要显式的拉取请求评审

对于代码:URL 是永久的

看看上述的前两个点,拉取请求促成了一个正在进行的代码讨论,使代码变更可以更醒目,并且使您很容易在审查的过程中找到所需的代码。无论是对于新人还是有经验的开发人员,能够回顾以前的讨论,了解一个功能为什么以这种方式开发出来,或者与另一个相关功能的讨论相联系起来是无价的。当跨多个项目协调,并使每个人尽可能接近代码时,前后讨论的内容也非常重要。如果这些功能仍在开发中,重要的是能够看到上次审查以来更改了哪些内容。毕竟,审查小的更改要比大的容易得多,但不可能全都是小功能。因此,重要的是能够找到你上次审查,并只看到从那时以来的变化。

集成工具:软件开发人员的偏执

再看下上述第三点,GitHub 上的拉取请求有很多功能,但开发人员总是偏好第三方工具。代码质量是个完整的代码审查领域,它涉及到其它组件的代码评审,而这些评审不一定是由人完成的。检测“低效”或缓慢的代码、具有潜在安全漏洞或不符合公司标准的代码是留给自动化工具的任务。类似 SonarQubeCode Climatecan 这样工具可以分析你的代码,而像 CodecovCoveralls 的这样工具可以告诉你刚刚写的新代码还没有得到很好的测试。这些工具最令人称奇的是,它们可以集成到 GitHub 中,并把它们的发现汇报到拉取请求当中!这意味着该过程中不仅是人们在审查代码,而且工具也在会在那里报告情况。这样每个人都可以完全了解一个功能是如何开发的。

最后,根据您的团队的偏好,您可以利用受保护的分支工作流 必需状态 required status 功能来要求进行工具审查和同行评审。

虽然您可能只是刚刚开始您的软件开发之旅,或者是一位希望知道项目正在做什么的业务利益相关者,抑或是一位想要确保项目的及时性和质量的项目经理,你都可以通过设置批准流程来参与到拉取请求中,并考虑集成更多的工具以确保质量,这在任何级别的软件开发中都很重要。

无论是为您的个人网站,贵公司的在线商店,还是想用最新的组合以获得最大的收获,编写好的软件都需要进行良好的代码审查。良好的代码审查涉及到正确的工具和平台。要了解有关 GitHub 和软件开发过程的更多信息,请参阅 O'Reilly 的 《GitHub 简介》 一书, 您可以在其中了解创建项目、启动拉取请求以及概要了解团队的软件开发流程。


作者简介:

Brent Beer

Brent Beer 通过大学的课程、对开源项目的贡献,以及担任专业网站开发人员使用 Git 和 GitHub 已经超过五年了。在担任 GitHub 上的培训师时,他也成为 O’Reilly 的 《GitHub 简介》的出版作者。他在阿姆斯特丹担任 GitHub 的解决方案工程师,帮助 Git 和 GitHub 向世界各地的开发人员提供服务。

Peter Bell

Peter Bell 是 Ronin 实验室的创始人以及 CTO。培训是存在问题的,我们通过技术提升培训来改进它!他是一位有经验的企业家、技术专家、敏捷教练和 CTO,专门从事 EdTech 项目。他为 O'Reilly 撰写了 《GitHub 简介》,为代码学校创建了“精通 GitHub ”课程,为 Pearson 创建了“ Git 和 GitHub 现场课”课程。他经常在国际和国际会议上发表 ruby、 nodejs、NoSQL(尤其是 MongoDB 和 neo4j )、云计算、软件工艺、java、groovy、j 等的演讲。


via: https://www.oreilly.com/ideas/how-to-use-pull-requests-to-improve-your-code-reviews

作者:Brent Beer, Peter Bell 译者:MonkeyDEcho 校对:wxy

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

摘要

在 Chrome 59 中开始搭载 Headless Chrome。这是一种在 无需显示 headless 的环境下运行 Chrome 浏览器的方式。从本质上来说,就是不用 chrome 浏览器来运行 Chrome 的功能!它将 Chromium 和 Blink 渲染引擎提供的所有现代 Web 平台的功能都带入了命令行。

它有什么用?

无需显示 headless 的浏览器对于自动化测试和不需要可视化 UI 界面的服务器环境是一个很好的工具。例如,你可能需要对真实的网页运行一些测试,创建一个 PDF,或者只是检查浏览器如何呈现 URL。

注意: Mac 和 Linux 上的 Chrome 59 都可以运行无需显示模式。对 Windows 的支持将在 Chrome 60 中提供。要检查你使用的 Chrome 版本,请在浏览器中打开 chrome://version

开启 无需显示 headless 模式(命令行界面)

开启 无需显示 headless 模式最简单的方法是从命令行打开 Chrome 二进制文件。如果你已经安装了 Chrome 59 以上的版本,请使用 --headless 标志启动 Chrome:

chrome \
  --headless \                   # Runs Chrome in headless mode.
  --disable-gpu \                # Temporarily needed for now.
  --remote-debugging-port=9222 \
  https://www.chromestatus.com   # URL to open. Defaults to about:blank.
注意:目前你仍然需要使用 --disable-gpu 标志。但它最终会不需要的。

chrome 二进制文件应该指向你安装 Chrome 的位置。确切的位置会因平台差异而不同。当前我在 Mac 上操作,所以我为安装的每个版本的 Chrome 都创建了方便使用的别名。

如果您使用 Chrome 的稳定版,并且无法获得测试版,我建议您使用 chrome-canary 版本:

alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"
alias chrome-canary="/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary"
alias chromium="/Applications/Chromium.app/Contents/MacOS/Chromium"

这里下载 Chrome Cannary。

命令行的功能

在某些情况下,你可能不需要以脚本编程的方式操作 Headless Chrome。可以使用一些有用的命令行标志来执行常见的任务。

打印 DOM

--dump-dom 标志将打印 document.body.innerHTML 到标准输出:

chrome --headless --disable-gpu --dump-dom https://www.chromestatus.com/

创建一个 PDF

--print-to-pdf 标志将页面转出为 PDF 文件:

chrome --headless --disable-gpu --print-to-pdf https://www.chromestatus.com/

截图

要捕获页面的屏幕截图,请使用 --screenshot 标志:

chrome --headless --disable-gpu --screenshot https://www.chromestatus.com/

# Size of a standard letterhead.
chrome --headless --disable-gpu --screenshot --window-size=1280,1696 https://www.chromestatus.com/

# Nexus 5x
chrome --headless --disable-gpu --screenshot --window-size=412,732 https://www.chromestatus.com/

使用 --screenshot 标志运行 Headless Chrome 将在当前工作目录中生成一个名为 screenshot.png 的文件。如果你正在寻求整个页面的截图,那么会涉及到很多事情。来自 David Schnurr 的一篇很棒的博文已经介绍了这一内容。请查看 使用 headless Chrome 作为自动截屏工具

REPL 模式 (read-eval-print loop)

--repl 标志可以使 Headless Chrome 运行在一个你可以使用浏览器评估 JS 表达式的模式下。执行下面的命令:

$ chrome --headless --disable-gpu --repl https://www.chromestatus.com/
[0608/112805.245285:INFO:headless_shell.cc(278)] Type a Javascript expression to evaluate or "quit" to exit.
>>> location.href
{"result":{"type":"string","value":"https://www.chromestatus.com/features"}}
>>> quit

在没有浏览器界面的情况下调试 Chrome

当你使用 --remote-debugging-port=9222 运行 Chrome 时,它会启动一个支持 DevTools 协议的实例。该协议用于与 Chrome 进行通信,并且驱动 Headless Chrome 浏览器实例。它也是一个类似 Sublime、VS Code 和 Node 的工具,可用于应用程序的远程调试。#协同效应

由于你没有浏览器用户界面可用来查看网页,请在另一个浏览器中输入 http://localhost:9222,以检查一切是否正常。你将会看到一个 可检查的 inspectable 页面的列表,可以点击它们来查看 Headless Chrome 正在呈现的内容:

DevTools 远程调试界面

从这里,你就可以像往常一样使用熟悉的 DevTools 来检查、调试和调整页面了。如果你以编程方式使用 Headless Chrome,这个页面也是一个功能强大的调试工具,用于查看所有通过网络与浏览器交互的原始 DevTools 协议命令。

使用编程模式 (Node)

Puppeteer 库 API

Puppeteer 是一个由 Chrome 团队开发的 Node 库。它提供了一个高层次的 API 来控制无需显示版(或 完全版)的 Chrome。它与其他自动化测试库,如 Phantom 和 NightmareJS 相类似,但是只适用于最新版本的 Chrome。

除此之外,Puppeteer 还可用于轻松截取屏幕截图,创建 PDF,页面间导航以及获取有关这些页面的信息。如果你想快速地自动化进行浏览器测试,我建议使用该库。它隐藏了 DevTools 协议的复杂性,并可以处理诸如启动 Chrome 调试实例等繁冗的任务。

安装:

yarn add puppeteer

例子 - 打印用户代理:

const puppeteer = require('puppeteer');

(async() => {
  const browser = await puppeteer.launch();
  console.log(await browser.version());
  browser.close();
})();

例子 - 获取页面的屏幕截图:

const puppeteer = require('puppeteer');

(async() => {

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://www.chromestatus.com', {waitUntil: 'networkidle'});
await page.pdf({path: 'page.pdf', format: 'A4'});

browser.close();
})();

查看 Puppeteer 的文档,了解完整 API 的更多信息。

CRI 库

chrome-remote-interface 是一个比 Puppeteer API 更低层次的库。如果你想要更接近原始信息和更直接地使用 DevTools 协议的话,我推荐使用它。

启动 Chrome

chrome-remote-interface 不会为你启动 Chrome,所以你要自己启动它。

在前面的 CLI 章节中,我们使用 --headless --remote-debugging-port=9222 手动启动了 Chrome。但是,要想做到完全自动化测试,你可能希望从你的应用程序中启动 Chrome。

其中一种方法是使用 child_process

const execFile = require('child_process').execFile;

function launchHeadlessChrome(url, callback) {
  // Assuming MacOSx.
  const CHROME = '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome';
  execFile(CHROME, ['--headless', '--disable-gpu', '--remote-debugging-port=9222', url], callback);
}

launchHeadlessChrome('https://www.chromestatus.com', (err, stdout, stderr) => {
  ...
});

但是如果你想要在多个平台上运行可移植的解决方案,事情会变得很棘手。请注意 Chrome 的硬编码路径:

使用 ChromeLauncher

Lighthouse 是一个令人称奇的网络应用的质量测试工具。Lighthouse 内部开发了一个强大的用于启动 Chrome 的模块,现在已经被提取出来单独使用。chrome-launcher NPM 模块 可以找到 Chrome 的安装位置,设置调试实例,启动浏览器和在程序运行完之后将其杀死。它最好的一点是可以跨平台工作,感谢 Node!

默认情况下,chrome-launcher 会尝试启动 Chrome Canary(如果已经安装),但是你也可以更改它,手动选择使用的 Chrome 版本。要想使用它,首先从 npm 安装:

yarn add chrome-launcher

例子 - 使用 chrome-launcher 启动 Headless Chrome:

const chromeLauncher = require('chrome-launcher');

// Optional: set logging level of launcher to see its output.
// Install it using: yarn add lighthouse-logger
// const log = require('lighthouse-logger');
// log.setLevel('info');

/**
 * Launches a debugging instance of Chrome.
 * @param {boolean=} headless True (default) launches Chrome in headless mode.
 *     False launches a full version of Chrome.
 * @return {Promise<ChromeLauncher>}
 */
function launchChrome(headless=true) {
  return chromeLauncher.launch({
    // port: 9222, // Uncomment to force a specific port of your choice.
    chromeFlags: [
      '--window-size=412,732',
      '--disable-gpu',
      headless ? '--headless' : ''
    ]
  });
}

launchChrome().then(chrome => {
  console.log(`Chrome debuggable on port: ${chrome.port}`);
  ...
  // chrome.kill();
});

运行这个脚本没有做太多的事情,但你应该能在任务管理器中看到启动了一个 Chrome 的实例,它加载了页面 about:blank。记住,它不会有任何的浏览器界面,我们是无需显示的。

为了控制浏览器,我们需要 DevTools 协议!

检索有关页面的信息

警告: DevTools 协议可以做一些有趣的事情,但是起初可能有点令人生畏。我建议先花点时间浏览 DevTools 协议查看器。然后,转到 chrome-remote-interface 的 API 文档,看看它是如何包装原始协议的。

我们来安装该库:

yarn add chrome-remote-interface

例子 - 打印用户代理:

const CDP = require('chrome-remote-interface');

...

launchChrome().then(async chrome => {
  const version = await CDP.Version({port: chrome.port});
  console.log(version['User-Agent']);
});

结果是类似这样的东西:HeadlessChrome/60.0.3082.0

例子 - 检查网站是否有 Web 应用程序清单

const CDP = require('chrome-remote-interface');

...

(async function() {

const chrome = await launchChrome();
const protocol = await CDP({port: chrome.port});

// Extract the DevTools protocol domains we need and enable them.
// See API docs: https://chromedevtools.github.io/devtools-protocol/
const {Page} = protocol;
await Page.enable();

Page.navigate({url: 'https://www.chromestatus.com/'});

// Wait for window.onload before doing stuff.
Page.loadEventFired(async () => {
  const manifest = await Page.getAppManifest();

  if (manifest.url) {
    console.log('Manifest: ' + manifest.url);
    console.log(manifest.data);
  } else {
    console.log('Site has no app manifest');
  }

  protocol.close();
  chrome.kill(); // Kill Chrome.
});

})();

例子 - 使用 DOM API 提取页面的 <title>

const CDP = require('chrome-remote-interface');

...

(async function() {

const chrome = await launchChrome();
const protocol = await CDP({port: chrome.port});

// Extract the DevTools protocol domains we need and enable them.
// See API docs: https://chromedevtools.github.io/devtools-protocol/
const {Page, Runtime} = protocol;
await Promise.all([Page.enable(), Runtime.enable()]);

Page.navigate({url: 'https://www.chromestatus.com/'});

// Wait for window.onload before doing stuff.
Page.loadEventFired(async () => {
  const js = "document.querySelector('title').textContent";
  // Evaluate the JS expression in the page.
  const result = await Runtime.evaluate({expression: js});

  console.log('Title of page: ' + result.result.value);

  protocol.close();
  chrome.kill(); // Kill Chrome.
});

})();

使用 Selenium、WebDriver 和 ChromeDriver

现在,Selenium 开启了 Chrome 的完整实例。换句话说,这是一个自动化的解决方案,但不是完全无需显示的。但是,Selenium 只需要进行小小的配置即可运行 Headless Chrome。如果你想要关于如何自己设置的完整说明,我建议你阅读“使用 Headless Chrome 来运行 Selenium”,不过你可以从下面的一些示例开始。

使用 ChromeDriver

ChromeDriver 2.3.0 支持 Chrome 59 及更新版本,可与 Headless Chrome 配合使用。在某些情况下,你可能需要等到 Chrome 60 以解决 bug。例如,Chrome 59 中屏幕截图已知存在问题。

安装:

yarn add selenium-webdriver chromedriver

例子:

const fs = require('fs');
const webdriver = require('selenium-webdriver');
const chromedriver = require('chromedriver');

// This should be the path to your Canary installation.
// I'm assuming Mac for the example.
const PATH_TO_CANARY = '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary';

const chromeCapabilities = webdriver.Capabilities.chrome();
chromeCapabilities.set('chromeOptions', {
  binary: PATH_TO_CANARY // Screenshots require Chrome 60\. Force Canary.
  'args': [
    '--headless',
  ]
});

const driver = new webdriver.Builder()
  .forBrowser('chrome')
  .withCapabilities(chromeCapabilities)
  .build();

// Navigate to google.com, enter a search.
driver.get('https://www.google.com/');
driver.findElement({name: 'q'}).sendKeys('webdriver');
driver.findElement({name: 'btnG'}).click();
driver.wait(webdriver.until.titleIs('webdriver - Google Search'), 1000);

// Take screenshot of results page. Save to disk.
driver.takeScreenshot().then(base64png => {
  fs.writeFileSync('screenshot.png', new Buffer(base64png, 'base64'));
});

driver.quit();

使用 WebDriverIO

WebDriverIO 是一个在 Selenium WebDrive 上构建的更高层次的 API。

安装:

yarn add webdriverio chromedriver

例子:过滤 chromestatus.com 上的 CSS 功能:

const webdriverio = require('webdriverio');
const chromedriver = require('chromedriver');

// This should be the path to your Canary installation.
// I'm assuming Mac for the example.
const PATH_TO_CANARY = '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary';
const PORT = 9515;

chromedriver.start([
  '--url-base=wd/hub',
  `--port=${PORT}`,
  '--verbose'
]);

(async () => {

const opts = {
  port: PORT,
  desiredCapabilities: {
    browserName: 'chrome',
    chromeOptions: {
      binary: PATH_TO_CANARY // Screenshots require Chrome 60\. Force Canary.
      args: ['--headless']
    }
  }
};

const browser = webdriverio.remote(opts).init();

await browser.url('https://www.chromestatus.com/features');

const title = await browser.getTitle();
console.log(`Title: ${title}`);

await browser.waitForText('.num-features', 3000);
let numFeatures = await browser.getText('.num-features');
console.log(`Chrome has ${numFeatures} total features`);

await browser.setValue('input[type="search"]', 'CSS');
console.log('Filtering features...');
await browser.pause(1000);

numFeatures = await browser.getText('.num-features');
console.log(`Chrome has ${numFeatures} CSS features`);

const buffer = await browser.saveScreenshot('screenshot.png');
console.log('Saved screenshot...');

chromedriver.stop();
browser.end();

})();

更多资源

以下是一些可以带你入门的有用资源:

文档

工具

样例

  • "The Headless Web" - Paul Kinlan 发布的使用了 Headless 和 api.ai 的精彩博客

常见问题

我需要 --disable-gpu 标志吗?

目前是需要的。--disable-gpu 标志在处理一些 bug 时是需要的。在未来版本的 Chrome 中就不需要了。查看 https://crbug.com/546953#c152https://crbug.com/695212 获取更多信息。

所以我仍然需要 Xvfb 吗?

不。Headless Chrome 不使用窗口,所以不需要像 Xvfb 这样的显示服务器。没有它你也可以愉快地运行你的自动化测试。

什么是 Xvfb?Xvfb 是一个用于类 Unix 系统的运行于内存之内的显示服务器,可以让你运行图形应用程序(如 Chrome),而无需附加的物理显示器。许多人使用 Xvfb 运行早期版本的 Chrome 进行 “headless” 测试。

如何创建一个运行 Headless Chrome 的 Docker 容器?

查看 lighthouse-ci。它有一个使用 Ubuntu 作为基础镜像的 Dockerfile 示例,并且在 App Engine Flexible 容器中安装和运行了 Lighthouse。

我可以把它和 Selenium / WebDriver / ChromeDriver 一起使用吗?

是的。查看 Using Selenium, WebDrive, or ChromeDriver

它和 PhantomJS 有什么关系?

Headless Chrome 和 PhantomJS 是类似的工具。它们都可以用来在无需显示的环境中进行自动化测试。两者的主要不同在于 Phantom 使用了一个较老版本的 WebKit 作为它的渲染引擎,而 Headless Chrome 使用了最新版本的 Blink。

目前,Phantom 提供了比 DevTools protocol 更高层次的 API。

我在哪儿提交 bug?

对于 Headless Chrome 的 bug,请提交到 crbug.com

对于 DevTools 协议的 bug,请提交到 github.com/ChromeDevTools/devtools-protocol


作者简介

Eric Bidelman 谷歌工程师,Lighthouse 开发,Web 和 Web 组件开发,Chrome 开发


via: https://developers.google.com/web/updates/2017/04/headless-chrome

作者:Eric Bidelman 译者:firmianay 校对:wxy

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

简单说来,一个 编译器 compiler 不过是一个可以翻译其他程序的程序。传统的编译器可以把源代码翻译成你的计算机能够理解的可执行机器代码。(一些编译器将源代码翻译成别的程序语言,这样的编译器称为源到源翻译器或 转化器 transpilers 。)LLVM 是一个广泛使用的编译器项目,包含许多模块化的编译工具。

传统的编译器设计包含三个部分:

  • 前端 Frontend 将源代码翻译为 中间表示 intermediate representation (IR)* 。clang 是 LLVM 中用于 C 家族语言的前端工具。
  • 优化器 Optimizer 分析 IR 然后将其转化为更高效的形式。opt 是 LLVM 的优化工具。
  • 后端 Backend 通过将 IR 映射到目标硬件指令集从而生成机器代码。llc 是 LLVM 的后端工具。

注:LLVM 的 IR 是一种和汇编类似的低级语言。然而,它抽离了特定硬件信息。

Hello, Compiler

下面是一个打印 “Hello, Compiler!” 到标准输出的简单 C 程序。C 语法是人类可读的,但是计算机却不能理解,不知道该程序要干什么。我将通过三个编译阶段使该程序变成机器可执行的程序。

// compile_me.c
// Wave to the compiler. The world can wait.

#include <stdio.h>

int main() {
  printf("Hello, Compiler!\n");
  return 0;
}

前端

正如我在上面所提到的,clang 是 LLVM 中用于 C 家族语言的前端工具。Clang 包含 C 预处理器 C preprocessor 词法分析器 lexer 语法解析器 parser 语义分析器 semantic analyzer IR 生成器 IR generator

C 预处理器在将源程序翻译成 IR 前修改源程序。预处理器处理外部包含文件,比如上面的 #include <stdio.h>。 它将会把这一行替换为 stdio.h C 标准库文件的完整内容,其中包含 printf 函数的声明。

通过运行下面的命令来查看预处理步骤的输出:

clang -E compile_me.c -o preprocessed.i

词法分析器(或 扫描器 scanner 分词器 tokenizer )将一串字符转化为一串单词。每一个单词或 记号 token ,被归并到五种语法类别之一:标点符号、关键字、标识符、文字或注释。

compile\_me.c 的分词过程:

语法分析器确定源程序中的单词流是否组成了合法的句子。在分析记号流的语法后,它会输出一个 抽象语法树 abstract syntax tree (AST)。Clang 的 AST 中的节点表示声明、语句和类型。

compile\_me.c 的语法树:

语义分析器会遍历抽象语法树,从而确定代码语句是否有正确意义。这个阶段会检查类型错误。如果 compile_me.c 的 main 函数返回 "zero"而不是 0, 那么语义分析器将会抛出一个错误,因为 "zero" 不是 int 类型。

IR 生成器将抽象语法树翻译为 IR。

对 compile\_me.c 运行 clang 来生成 LLVM IR:

clang -S -emit-llvm -o llvm_ir.ll compile_me.c

llvm_ir.ll 中的 main 函数:

; llvm_ir.ll
@.str = private unnamed_addr constant [18 x i8] c"Hello, Compiler!\0A\00", align 1

define i32 @main() {
  %1 = alloca i32, align 4 ; <- memory allocated on the stack
  store i32 0, i32* %1, align 4
  %2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([18 x i8], [18 x i8]* @.str, i32 0, i32 0))
  ret i32 0
}

declare i32 @printf(i8*, ...)

优化程序

优化程序的工作是基于其对程序的运行时行为的理解来提高代码效率。优化程序将 IR 作为输入,然后生成改进后的 IR 作为输出。LLVM 的优化工具 opt 将会通过标记 -O2(大写字母 o,数字 2)来优化处理器速度,通过标记 Os(大写字母 o,小写字母 s)来减少指令数目。

看一看上面的前端工具生成的 LLVM IR 代码和运行下面的命令生成的结果之间的区别:

opt -O2 -S llvm_ir.ll -o optimized.ll

optimized.ll 中的 main 函数:

optimized.ll

@str = private unnamed_addr constant [17 x i8] c"Hello, Compiler!\00"

define i32 @main() {
  %puts = tail call i32 @puts(i8* getelementptr inbounds ([17 x i8], [17 x i8]* @str, i64 0, i64 0))
  ret i32 0
}

declare i32 @puts(i8* nocapture readonly)

优化后的版本中, main 函数没有在栈中分配内存,因为它不使用任何内存。优化后的代码中调用 puts 函数而不是 printf 函数,因为程序中并没有使用 printf 函数的格式化功能。

当然,优化程序不仅仅知道何时可以把 printf 函数用 puts 函数代替。优化程序也能展开循环并内联简单计算的结果。考虑下面的程序,它将两个整数相加并打印出结果。

// add.c
#include <stdio.h>

int main() {
  int a = 5, b = 10, c = a + b;
  printf("%i + %i = %i\n", a, b, c);
}

下面是未优化的 LLVM IR:

@.str = private unnamed_addr constant [14 x i8] c"%i + %i = %i\0A\00", align 1

define i32 @main() {
  %1 = alloca i32, align 4 ; <- allocate stack space for var a
  %2 = alloca i32, align 4 ; <- allocate stack space for var b
  %3 = alloca i32, align 4 ; <- allocate stack space for var c
  store i32 5, i32* %1, align 4  ; <- store 5 at memory location %1
  store i32 10, i32* %2, align 4 ; <- store 10 at memory location %2
  %4 = load i32, i32* %1, align 4 ; <- load the value at memory address %1 into register %4
  %5 = load i32, i32* %2, align 4 ; <- load the value at memory address %2 into register %5
  %6 = add nsw i32 %4, %5 ; <- add the values in registers %4 and %5\. put the result in register %6
  store i32 %6, i32* %3, align 4 ; <- put the value of register %6 into memory address %3
  %7 = load i32, i32* %1, align 4 ; <- load the value at memory address %1 into register %7
  %8 = load i32, i32* %2, align 4 ; <- load the value at memory address %2 into register %8
  %9 = load i32, i32* %3, align 4 ; <- load the value at memory address %3 into register %9
  %10 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0), i32 %7, i32 %8, i32 %9)
  ret i32 0
}

declare i32 @printf(i8*, ...)

下面是优化后的 LLVM IR:

@.str = private unnamed_addr constant [14 x i8] c"%i + %i = %i\0A\00", align 1

define i32 @main() {
  %1 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i64 0, i64 0), i32 5, i32 10, i32 15)
  ret i32 0
}

declare i32 @printf(i8* nocapture readonly, ...)

优化后的 main 函数本质上是未优化版本的第 17 行和 18 行,伴有变量值内联。opt 计算加法,因为所有的变量都是常数。很酷吧,对不对?

后端

LLVM 的后端工具是 llc。它分三个阶段将 LLVM IR 作为输入生成机器代码。

  • 指令选择是将 IR 指令映射到目标机器的指令集。这个步骤使用虚拟寄存器的无限名字空间。
  • 寄存器分配是将虚拟寄存器映射到目标体系结构的实际寄存器。我的 CPU 是 x86 结构,它只有 16 个寄存器。然而,编译器将会尽可能少的使用寄存器。
  • 指令安排是重排操作,从而反映出目标机器的性能约束。

运行下面这个命令将会产生一些机器代码:

llc -o compiled-assembly.s optimized.ll
_main:
    pushq   %rbp
    movq    %rsp, %rbp
    leaq    L_str(%rip), %rdi
    callq   _puts
    xorl    %eax, %eax
    popq    %rbp
    retq
L_str:
    .asciz  "Hello, Compiler!"

这个程序是 x86 汇编语言,它是计算机所说的语言,并具有人类可读语法。某些人最后也许能理解我。


相关资源:

  1. 设计一个编译器
  2. 开始探索 LLVM 核心库

(题图:deviantart.net)


via: https://nicoleorchard.com/blog/compilers

作者:Nicole Orchard 译者:ucasFL 校对:wxy

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