Go 语言没有内置 abs() 标准函数来计算整数的绝对值,这里的绝对值是指负数、正数的非负表示。

我最近为了解决 Advent of Code 2017 上边的 Day 20 难题,自己实现了一个 abs() 函数。如果你想学点新东西或试试身手,可以去一探究竟。

Go 实际上已经在 math 包中实现了 abs() : math.Abs ,但对我的问题并不适用,因为它的输入输出的值类型都是 float64,我需要的是 int64。通过参数转换是可以使用的,不过将 float64 转为 int64 会产生一些开销,且转换值很大的数会发生截断,这两点都会在文章说清楚。

帖子 Pure Go math.Abs outperforms assembly version 讨论了针对浮点数如何优化 math.Abs,不过这些优化的方法因底层编码不同,不能直接应用在整型上。

文章中的源码和测试用例在 cavaliercoder/go-abs

类型转换 VS 分支控制的方法

对我来说取绝对值最简单的函数实现是:输入参数 n 大于等于 0 直接返回 n,小于零则返回 -n(负数取反为正),这个取绝对值的函数依赖分支控制结构来计算绝对值,就命名为:abs.WithBranch

成功返回 n 的绝对值,这就是 Go v1.9.x math.Abs 对 float64 取绝对值的实现。不过当进行类型转换(int64 to float64)再取绝对值时,1.9.x 是否做了改进?我们可以验证一下:

上边的代码中,将 n 先从 int64 转成 float64,通过 math.Abs 取到绝对值后再转回 int64,多次转换显然会造成性能开销。可以写一个基准测试来验证一下:

$ go test -bench=.goos: darwingoarch: amd64pkg: github.com/cavaliercoder/absBenchmarkWithBranch-8 2000000000 0.30 ns/opBenchmarkWithStdLib-8 2000000000 0.79 ns/opPASSok github.com/cavaliercoder/abs 2.320s

测试结果:0.3 ns/op, WithBranch 要快两倍多,它还有一个优势:在将 int64 的大数转化为 IEEE-754 标准的 float64 不会发生截断(丢失超出精度的值)

举个例子:abs.WithBranch(-9223372036854775807) 会正确返回 9223372036854775807。但 WithStdLib(-9223372036854775807) 则在类型转换区间发生了溢出,返回 -9223372036854775808,在大的正数输入时, WithStdLib(9223372036854775807) 也会返回不正确的负数结果。

不依赖分支控制的方法取绝对值的方法对有符号整数显然更快更准,不过还有更好的办法吗?

我们都知道不依赖分支控制的方法的代码破坏了程序的运行顺序,即 pipelining processors 无法预知程序的下一步动作。

与不依赖分支控制的方法不同的方案

Hacker’s Delight 第二章介绍了一种无分支控制的方法,通过 Two’s Complement 计算有符号整数的绝对值。

为计算 x 的绝对值,先计算 x >> 63 ,即 x 右移 63 位(获取最高位符号位),如果你对熟悉无符号整数的话, 应该知道如果 x 是负数则 y 是 1,否者 y 为 0

接着再计算 (x ⨁ y) - y :x 与 y 异或后减 y,即是 x 的绝对值。

可以直接使用高效的汇编实现,代码如下:

我们先命名这个函数为 WithASM,分离命名与实现,函数体使用 Go 的汇编 实现,上边的代码只适用于 AMD64 架构的系统,我建议你的文件名加上 _amd64.s 的后缀。

WithASM 的基准测试结果:

$ go test -bench=.goos: darwingoarch: amd64pkg: github.com/cavaliercoder/absBenchmarkWithBranch-8 2000000000 0.29 ns/opBenchmarkWithStdLib-8 2000000000 0.78 ns/opBenchmarkWithASM-8 2000000000 1.78 ns/opPASSok github.com/cavaliercoder/abs 6.059s

这就比较尴尬了,这个简单的基准测试显示无分支控制结构高度简洁的代码跑起来居然很慢:1.78 ns/op,怎么会这样呢?

编译选项

我们需要知道 Go 的编译器是怎么优化执行 WithASM 函数的,编译器接受 -m 参数来打印出优化的内容,在 go build 或 go test中加上 -gcflags=-m 使用:

运行效果:

$ go tool compile -m abs.go# github.com/cavaliercoder/abs./abs.go:11:6: can inline WithBranch./abs.go:21:6: can inline WithStdLib./abs.go:22:23: inlining call to math.Abs

对于我们这个简单的函数,Go 的编译器支持 function inlining,函数内联是指在调用我们函数的地方直接使用这个函数的函数体来代替。举个例子:

实际上会被编译成:

根据编译器的输出,可以看出 WithBranch 和 WithStdLib 在编译时候被内联了,但是 WithASM 没有。对于 WithStdLib,即使底层调用了 math.Abs 但编译时依旧被内联。

因为 WithASM 函数没法内联,每个调用它的函数会在调用上产生额外的开销:为 WithASM 重新分配栈内存、复制参数及指针等等。

如果我们在其他函数中不使用内联会怎么样?可以写个简单的示例程序:

重新编译,我们会看到编译器优化内容变少了:

$ go tool compile -m abs.goabs.go:22:23: inlining call to math.Abs

基准测试的结果:

$ go test -bench=.goos: darwingoarch: amd64pkg: github.com/cavaliercoder/absBenchmarkWithBranch-8 1000000000 1.87 ns/opBenchmarkWithStdLib-8 1000000000 1.94 ns/opBenchmarkWithASM-8 2000000000 1.84 ns/opPASSok github.com/cavaliercoder/abs 8.122s

可以看出,现在三个函数的平均执行时间几乎都在 1.9 ns/op 左右。

你可能会觉得每个函数的调用开销在 1.5ns 左右,这个开销的出现否定了我们 WithBranch 函数中的速度优势。

我从上边学到的东西是, WithASM 的性能要优于编译器实现类型安全、垃圾回收和函数内联带来的性能,虽然大多数情况下这个结论可能是错误的。当然,这其中是有特例的,比如提升 SIMD 的加密性能、流媒体编码等。

只使用一个内联函数

Go 编译器无法内联由汇编实现的函数,但是内联我们重写后的普通函数是很容易的:

编译结果说明我们的方法被内联了:

$ go tool compile -m abs.go...abs.go:26:6: can inline WithTwosComplement

但是性能怎么样呢?结果表明:当我们启用函数内联时,性能与 WithBranch 很相近了:

$ go test -bench=.goos: darwingoarch: amd64pkg: github.com/cavaliercoder/absBenchmarkWithBranch-8 2000000000 0.29 ns/opBenchmarkWithStdLib-8 2000000000 0.79 ns/opBenchmarkWithTwosComplement-8 2000000000 0.29 ns/opBenchmarkWithASM-8 2000000000 1.83 ns/opPASSok github.com/cavaliercoder/abs 6.777s

现在函数调用的开销消失了,WithTwosComplement 的实现要比 WithASM 的实现好得多。来看看编译器在编译 WithASM 时做了些什么?

使用 -S 参数告诉编译器打印出汇编过程:

编译器在编译 WithASM 和 WithTwosComplement 时,做的事情太像了,编译器在这时才有正确配置和跨平台的优势,可加上 GOARCH=386 选项再次编译生成兼容 32 位系统的程序。

最后关于内存分配,上边所有函数的实现都是比较理想的情况,我运行 go test -bench=. -benchme,观察对每个函数的输出,显示并没有发生内存分配。

总结

WithTwosComplement 的实现方式在 Go 中提供了较好的可移植性,同时实现了函数内联、无分支控制的代码、零内存分配与避免类型转换导致的值截断。基准测试没有显示出无分支控制比有分支控制的优势,但在理论上,无分支控制的代码在多种情况下性能会更好。

最后,我对 int64 的 abs 实现如下:

func abs(n int64) int64 { y := n >> 63 return (n ^ y) - y}

abs 不会整数 方法 溢出_在 Golang 中针对 int64 类型优化 abs()相关推荐

  1. Golang 中针对 int64 类型优化 abs()

    前言 Go 语言没有内置 abs() 标准函数来计算整数的绝对值,这里的绝对值是指负数.正数的非负表示. 我最近为了解决 Advent of Code 2017 上边的 Day 20 难题,自己实现了 ...

  2. abs 不会整数 方法 溢出_asp cint clng的范围与防止cint和clng的溢出解决方法大全

    首先我们需要了解的是 cint范围 -32,768 到 32,767. clng范围 -2,147,483,648 到 2,147,483,647. cint与clng含义: 都可以强制将一个表达式转 ...

  3. java中的方法求和_在Java中模拟求和类型的巧妙解决方法

    java中的方法求和 在继续阅读实际文章之前,我想感谢令人敬畏的Javaslang库的作者Daniel Dietrich ,他在我面前有了这个主意: @lukaseder尝试使用静态方法<T,T ...

  4. 缓冲区溢出_在Java中使用Google的协议缓冲区

    缓冲区溢出 最近发布了有效的Java第三版,我一直对确定此类Java开发书籍的更新感兴趣,该书籍的最新版本仅通过Java 6进行了介绍. 在此版本中,显然存在与Java 7 , Java 8和Java ...

  5. golang 函数传多个参数_关于Golang中方法参数的传递

    结构体声明 为了说明函数以及方法调用的过程,这里先定义一个struct,在下面的描述中会使用到它. type Person struct { Name string Age uint16 } 普通函数 ...

  6. onclick如何调用含参函数_在 golang 中如何调用私有函数(绑定隐藏的标识符)

    名字在 golang 中的重要性和在其他任何一种语言是一样的.他们甚至含有语义的作用:在一个包的外部某个名字的可见性是由这个名字首字母是否是大写来决定的. 有时为了更好的组织代码或者在其他包使用某些隐 ...

  7. alxctools索引超出了数组界限_[译]V8中的数组类型

    译者:蒋海涛 JavaScript 对象可以和任何属性有关联.对象属性的名称可以包含任何字符.有趣的是 JavaScript 引擎可以选择名称为纯数字的属性来进行优化,而这个属性其实就是数组 inde ...

  8. mysql pmt函数怎么用_在Golang中如何正确地使用database/sql包访问数据库

    本文记录了我在实际工作中关于数据库操作上一些小经验,也是新手入门golang时我认为一定会碰到问题,没有什么高大上的东西,所以希望能抛砖引玉,也算是对这个问题的一次总结.其实我也是一个新手,机缘巧合几 ...

  9. go语言的iota是什么意思_关于Golang中的iota

    快速一览 iota是Golang中提供的一个简化常量和枚举编程的标识符,合理的使用这个标识符可以让代码变得更简洁,省去大量的不必要的代码. 比如下面的这个常量定义 const ( a = 1 b = ...

最新文章

  1. 利用属性封装复杂的选项
  2. android地图定位
  3. 归档 OmniFocus 中已完成的任务到 印象笔记 Evernote
  4. 数字建模matlab,Matlab基础及数学建模.ppt
  5. Netty源码分析第6章(解码器)----第4节: 分隔符解码器
  6. Mysql:is not allowed to connect to this MySQL server
  7. 【错误记录】Kotlin 编译报错 ( Smart cast to ‘Xxx‘ is impossible, because ‘xxx‘ is a mutable property ... )
  8. C#中使用jieba.NET、WordCloudSharp制作词云图
  9. java图片上传及图片回显1
  10. B-树关键字个数计算
  11. 安装HP P1008打印机经历
  12. 计算机的编译原理pdf,计算机编译原理DK.pdf
  13. 认知水平高下定义及提高认知水平的方法
  14. Android基础之将毫秒换算成(天/时/分/秒/毫秒)
  15. 体系切换,华为IPD的研发管理之道(上)
  16. mysql 12点_MySQL 查询昨天中午12点到今天中午12点的数据
  17. excel中录制宏只执行一半的命令,没有执行全部如何解决?
  18. Eclipse中python的配置方法
  19. SpringBoot上传图片问题
  20. JavaScript实现H5游戏断线自动重连的技术

热门文章

  1. 从业务到平台的思维转变
  2. javascript清除map所占内存_【原创.54期】 JavaScript的V8引擎初探
  3. mysql oracle查询速度慢_oracle查看执行最慢与查询次数最多的sql语句
  4. gateway 内存溢出问题_带你学习jvm java虚拟机 arthas/性能调优/故障排除/gc回收/内存溢出等...
  5. cv::cuda::split 使用
  6. 高清变脸更快更逼真!比GAN更具潜力的可逆生成模型来了 | OpenAI论文+代码
  7. python 调用C++,传递int,char,char*,数组和多维数组
  8. python 单通道转3通道
  9. android自定义尺子收集demo
  10. BGP建立邻居的详细过程