go 的两个黑魔法技巧「终于解决」 -金沙1005

go 的两个黑魔法技巧「终于解决」u32toa_small 的实现也比较简单,使用查表法,如下:然后在 op.go 中加入对应的 __u32toa_small 函数:使用 cla

作者:pedrogao,腾讯csig后台研发工程师

最近,在写 go 代码的时候,发现了其特别有意思的两个奇技淫巧,于是写下这篇
文章和大家分享一下。

按照 go 的编译约定,代码包内以小写字母开头的函数、变量是私有的:

package test
// 私有
func abs() {}
// 公共
func abs() {}
go 的两个黑魔法技巧「终于解决」

为防止网络爬虫,请关注公众号回复”口令”

激活idea 激活clion
datagrip dataspell
dotcover dotmemory
dottrace goland
phpstorm pycharm
resharper reshac
rider rubymine
webstorm 全家桶

对于 test 包中 abs 函数只能在包内调用,而 abs 函数却可以在其它包中导入后使用。

私有变量、方法的意义在于封装:控制内部数据、保证外部交互的一致性。

这样既能促进系统运行的可靠性,也能减少使用者的信息负载。

这样的规定对设计、封装良好的包是友好的,但并不是每个人都有这样的能力,另外对于一些特殊的函数,如:runtime 中的 memmove 函数,在有些场景下,确实是需要的。

因此 go 在程序链接阶段给开发者打开了一扇窗,即可以通过 go:linkname 指令来链接包内的私有函数。

以 memmove 为例,
如下:

func memmove(to, from unsafe.pointer, n uintptr)

memmove 作为 runtime 中的私有函数,用于任意数据之间的内存拷贝,无视类型信息,直接操作内存,这样的操作在 go 中虽然是不提倡的,但是用好了,却也是一把利刃。

新建一个 go 文件,如 runtime.go,并加上如下内容:

//go:noescape
//go:linkname memmove runtime.memmove
//goland:noinspection gounusedparameter
func memmove(to unsafe.pointer, from unsafe.pointer, n uintptr)

把视角放到 go:linkname 指令上,该指令接受两个参数:

  • memmove:当前函数名称;
  • runtime.memmove:对应链接的函数的路径,报名 函数名。

这样,编译器在做链接时就会将当前的 memmove 函数链接到 runtime 中的 memmove 函数, 我们就能使用该函数了。

在平常写代码的时候,我们经常性地需要拷贝字节切片、字符串之间的数据。比如将数据从切片 1拷贝到切片 2,使用 memmove 代码如下:

// runtime.go
type goslice struct {
    ptr unsafe.pointer
    len int
    cap int
}
// runtime_test.go
func test_memmove(t *testing.t) {
 src := []byte{1, 2, 3, 4, 5, 6}
 dest := make([]byte, 10, 10)
 spew.dump(src)
 spew.dump(dest)
 srcp := (*goslice)(unsafe.pointer(&src))
 destp := (*goslice)(unsafe.pointer(&dest))
 memmove(destp.ptr, srcp.ptr, unsafe.sizeof(byte(0))*6)
 spew.dump(src)
 spew.dump(dest)
}

字节切片([]byte)在内存中的形态如 goslice 结构体来所示,lencap 分别表示切片长度、容量,字段 ptr 指向真实的字节数据。

将两个切片的数据指针以及拷贝长度作为参数传入 memmove,数据就能从 src 拷贝到 dest。运行结果如下:

=== run   test_memmove
# 拷贝之前
([]uint8) (len=6 cap=6) {
 00000000  01 02 03 04 05 06                                 |......|
}
([]uint8) (len=10 cap=10) {
 00000000  00 00 00 00 00 00 00 00  00 00                    |..........|
}
# 拷贝之后
([]uint8) (len=6 cap=6) {
 00000000  01 02 03 04 05 06                                 |......|
}
([]uint8) (len=10 cap=10) {
 00000000  01 02 03 04 05 06 00 00  00 00                    |..........|

显然,对于切片之间的数据拷贝,标准库提供的 copy 函数要更加方便一些:

func test_copy(t *testing.t) {
src := []byte{1, 2, 3, 4, 5, 6}
dest := make([]byte, 10, 10)
 spew.dump(src)
 spew.dump(dest)
 copy(dest, src)
 spew.dump(src)
 spew.dump(dest)
}

这样也能达到一样的效果,memmove 更加适合字符串(string)和数组切片之间的数据拷贝场景,如下:

// runtime.go
type gostring struct {
    ptr unsafe.pointer
    len int
}
// runtime_test.go
func test_memmove(t *testing.t) {
 str := "pedro"
 // 注意:这里的len不能为0,否则数据没有分配,就无法复制
 data := make([]byte, 10, 10)
 spew.dump(str)
 spew.dump(data)
 memmove((*goslice)(unsafe.pointer(&data)).ptr, (*gostring)(unsafe.pointer(&str)).ptr,
  unsafe.sizeof(byte(0))*5)
 spew.dump(str)
 spew.dump(data)
}

类似地,gostring 是字符串在内存中的表达形态,通过 memmove 函数就能快速的将字符数据从字符串拷贝到切片,反之亦然,运行结果如下:

# 拷贝之前
(string) (len=5) "pedro"
([]uint8) (len=10 cap=10) {
 00000000  00 00 00 00 00 00 00 00  00 00                    |..........|
}
# 拷贝之后
(string) (len=5) "pedro"
([]uint8) (len=10 cap=10) {
 00000000  70 65 64 72 6f 00 00 00  00 00                    |pedro.....|
}

切片是 go 中最常用的数据结构之一,对于切片扩容,go 只提供了 append 函数来隐式的扩容,但内部是通过调用 runtime 中的 growslice
函数来实现的:

func growslice(et *_type, old slice, cap int) slice

growslice 函数接受 3 个参数:

  • et:切片容器中的数据类型,如 int,_type 可以表示 go 中的任意类型;
  • old:旧切片;
  • cap:扩容后的切片容量。

扩容成功后,返回新的切片。

同样地,使用go:linkname来链接 runtime 中的 growslice 函数,如下:

// runtime.go
type gotype struct {
 size       uintptr
 ptrdata    uintptr
 hash       uint32
 flags      uint8
 align      uint8
 fieldalign uint8
 kindflags  uint8
 traits     unsafe.pointer
 gcdata     *byte
 str        int32
 ptrtoself  int32
}
// goeface 本质是 interface
type goeface struct {
 type  *gotype
 value unsafe.pointer
}
//go:linkname growslice runtime.growslice
//goland:noinspection gounusedparameter
func growslice(et *gotype, old goslice, cap int) goslice

growslice 函数的第一个参数 et 实际是 go 对所有类型的一个抽象数据结构——gotype

这里引入了 go 语言实现机制中的两个重要数据结构:

  • goeface:empty interface,即 interface{},空接口;
  • gotype:go 类型定义数据结构,可用于表示任意类型。

关于 goeface、goiface、gotype、goitab 都是 go 语言实现的核心数据结构,这里的内容很多,感兴趣的可以参考这里 。

这样,我们就能通过调用 growslice 函数来对切片进行手动扩容了,如下:

// runtime.go
func unpacktype(t reflect.type) *gotype {
 return (*gotype)((*goeface)(unsafe.pointer(&t)).value)
}
// runtime_test.go
func test_growslice(t *testing.t) {
 assert := assert.new(t)
 var typebyte = unpacktype(reflect.typeof(byte(0)))
 spew.dump(typebyte)
 dest := make([]byte, 0, 10)
 assert.equal(len(dest), 0)
 assert.equal(cap(dest), 10)
 ds := (*goslice)(unsafe.pointer(&dest))
 *ds = growslice(typebyte, *ds, 100)
 assert.equal(len(dest), 0)
 assert.equal(cap(dest), 112)
}

由于 growslice 的参数et类型在 runtime 中不可见,我们重新定义了 gotype 来表示,
并且通过反射的机制来拿到字节切片中的 gotype,然后调用 growslice 完成扩容工作。

运行程序:

--- pass: test_growslice (0.00s)
pass

注意一个点,growslice 传入的 cap 参数是 100,但是最后的扩容结果却是 112,这个是因为 growslice 会做一个 roundupsize 处理,感兴趣的同学可以参考这里 。

下面,我们再来看 go 的另外一个更加有趣的黑魔法。

通过 cgo,我们可以很方便地在 go 中调用 c 代码,如下:

/*
#include 
#include 
static void* sbrk(int size) {
 void *r = sbrk(size);
 if(r == (void *)-1){
    return null;
  }
 return r;
}
*/
import "c"
import (
 "fmt"
)
func main() {
 mem := c.sbrk(c.int(100))
 defer c.free(mem)
 fmt.println(mem)
}

运行程序,会得到如下输出:

0xba00000

cgo 是 go 与 c 之间的桥梁,让 go 可以享受 c 语言强大的系统编程能力,比如这里的 sbrk 会直接向
进程申请一段内存,而这段内存是不受 go gc 的影响的,因此我们必须手动地释放(free)掉它。

在一些特殊场景,比如全局缓存,为了避免数据被 gc 掉而导致缓存失效,那么可以尝试这样使用。

当然,这还不够 tricky,别忘了,c 语言是可以直接内联汇编的,同样地,我们也可以在 go 中内联汇编
试试,如下:

/*
#include 
static int add(int i, int j)
{
  int res = 0;
  __asm__ ("add %1, %2"
    : "=r" (res)
    : "r" (i), "0" (j)
  );
  return res;
}
*/
import "c"
import (
 "fmt"
)
func main() {
 r := c.add(c.int(2022), c.int(18))
 fmt.println(r)
}

运行程序,可以得到如下输出:

2040

cgo 虽然给了我们一座桥梁,但付出的代价也不小,具体的缺点可以参考这里。

对 cgo 感兴趣的同学可以参考这里 。

那么有没有一种方式可以回避掉 cgo 的缺点,答案自然是可以的。

这个方式其实很容易想到:不使用 cgo,而是使用 plan9,也就是 go 支持的汇编语言。

当然我们不是直接去写汇编,而是将 c 编译成汇编,然后再转化成 plan9 与 .go 代码一起编译。

编译的过程如下图所示:

go 的两个黑魔法技巧「终于解决」

而且 c 本身就是汇编的高级抽象,作为目前最强劲性能的存在,这种方式不仅回避了 cgo 的性能问题,
反而将程序性能提高了。过程如下:

首先,我们定义一个简单的 c 语言函数 isspace(判断字符为空):

// ./inner/op.h
#ifndef op_h
#define op_h
char isspace(char ch);
// ./inner/op.c
#include "op.h"
char isspace(char ch) {
    return ch == ' ' || ch == '\r' || ch == '\n' | ch == '\t';
}

然后,使用 clang 将其编译为汇编(注意:是 clang):

$ clang -mno-red-zone -fno-asynchronous-unwind-tables -fno-builtin -fno-exceptions \
-fno-rtti -fno-stack-protector -nostdlib -o3 -msse4 -mavx -mno-avx2 -duse_avx=1 \
 -duse_avx2=0 -s ./inner/*.c

编译成功后,会在 inner 文件夹下生成一个 op.s 汇编文件,大致如下:

	.section	__text,__text,regular,pure_instructions
	.build_version macos, 11, 0
	.globl	_isspace                        ## -- begin function isspace
	.p2align	4, 0x90
_isspace:                               ## @isspace
## �.0:
	pushq	%rbp
	movq	%rsp, %rbp
	movb	$1, %al
	cmpb	$13, %dil
	je	lbb0_3

clang 默认生成的汇编是 at&t 格式的,这种汇编 go 是无法编译的(gccgo 除外),因此这里有一步转换工作。

负责将 at&t 汇编转化成 plan9 汇编,而二者之间的语法差异其实是比较大的,因此这里借助一个转换asm2asm 工具 来完成。

asm2asm clone 到本地,然后运行:

$ git clone https://github.com/chenzhuoyu/asm2asm
$ ./tools/asm2asm.py ./op.s ./inner/op.s

执行后,会报错。原因在于,go 对于 plan9 汇编文件需要一个对应的 .go 声明文件来对应。

我们在 ./inner/op.h 文件中定义了 isspace 函数,因此需要新建一个同名的 op.go 文件来声明这个函数:

//go:nosplit
//go:noescape
//goland:noinspection gounusedparameter
func __isspace(ch byte) (ret byte)

然后再次运行 asm2asm 工具来生成汇编:

$ ./tools/asm2asm.py ./op.s ./inner/op.s
$ tree .
.
|__ inner
|   |__  op.c
|   |__ op.h
|   |__ op.s
|__ op.go
|__ op.s
|__ op_subr.go

asm2asm 会生成两个文件:op.sop_subr.go

  • op.s:翻译而来的 plan9 汇编文件;
  • op_subr.go:函数调用辅助文件。

生成后,op.go 中的 __isspace 函数就能顺利的链接上对应的汇编代码,并运行,如下:

func test___isspace(t *testing.t) {
 type args struct {
  ch byte
 }
 tests := []struct {
  name    string
  args    args
  wantret byte
 }{
  {
   name:    "false",
   args:    args{ch: '0'},
   wantret: 0,
  },
  {
   name:    "true",
   args:    args{ch: '\n'},
   wantret: 1,
  },
 }
 for _, tt := range tests {
  t.run(tt.name, func(t *testing.t) {
   if gotret := __isspace(tt.args.ch); gotret != tt.wantret {
    t.errorf("__isspace() = %v, want %v", gotret, tt.wantret)
   }
  })
 }
}
// output
=== run   test___isspace
=== run   test___isspace/false
=== run   test___isspace/true
--- pass: test___isspace (0.00s)
    --- pass: test___isspace/false (0.00s)
    --- pass: test___isspace/true (0.00s)
pass

__isspace 顺利运行,并通过了单测。

一个 isspace 函数有些简单,无法完全发挥出汇编的能力,下面我们来看一个稍微复杂一点的例子:将整数转化为字符串。

在 go 中,整数转化为字符串的方式有多种,比如说:strconv.itoa 函数。

这里,我选择用 c 来写一个简单的整数转字符串的函数:u32toa_small,然后将其编译为汇编代码供 go 调用,并看看二者之间的性能差异。

u32toa_small 的实现也比较简单,使用查表法(strconv.itoa 使用的也是这种方法),如下:

#include "op.h"
static const char digits[200] = {
    '0', '0', '0', '1', '0', '2', '0', '3', '0', '4', '0', '5', '0', '6', '0', '7', '0', '8', '0', '9',
    '1', '0', '1', '1', '1', '2', '1', '3', '1', '4', '1', '5', '1', '6', '1', '7', '1', '8', '1', '9',
    '2', '0', '2', '1', '2', '2', '2', '3', '2', '4', '2', '5', '2', '6', '2', '7', '2', '8', '2', '9',
    '3', '0', '3', '1', '3', '2', '3', '3', '3', '4', '3', '5', '3', '6', '3', '7', '3', '8', '3', '9',
    '4', '0', '4', '1', '4', '2', '4', '3', '4', '4', '4', '5', '4', '6', '4', '7', '4', '8', '4', '9',
    '5', '0', '5', '1', '5', '2', '5', '3', '5', '4', '5', '5', '5', '6', '5', '7', '5', '8', '5', '9',
    '6', '0', '6', '1', '6', '2', '6', '3', '6', '4', '6', '5', '6', '6', '6', '7', '6', '8', '6', '9',
    '7', '0', '7', '1', '7', '2', '7', '3', '7', '4', '7', '5', '7', '6', '7', '7', '7', '8', '7', '9',
    '8', '0', '8', '1', '8', '2', '8', '3', '8', '4', '8', '5', '8', '6', '8', '7', '8', '8', '8', '9',
    '9', '0', '9', '1', '9', '2', '9', '3', '9', '4', '9', '5', '9', '6', '9', '7', '9', '8', '9', '9',
};
// < 10000
int u32toa_small(char *out, uint32_t val) {
    int      n  = 0;
    uint32_t d1 = (val / 100) << 1;
    uint32_t d2 = (val % 100) << 1;
    /* 1000-th digit */
    if (val >= 1000) {
        out[n  ] = digits[d1];
    }
    /* 100-th digit */
    if (val >= 100) {
        out[n  ] = digits[d1   1];
    }
    /* 10-th digit */
    if (val >= 10) {
        out[n  ] = digits[d2];
    }
    /* last digit */
    out[n  ] = digits[d2   1];
    return n;
}

然后在 op.go 中加入对应的 __u32toa_small 函数:

// < 10000
//go:nosplit
//go:noescape
//goland:noinspection gounusedparameter
func __u32toa_small(out *byte, val uint32) (ret int)

使用 clang 重新编译 op.c 文件,并用 asm2asm 工具来生成对应的汇编代码(节选部分):

_u32toa_small:
	byte $0x55  // pushq        %rbp
	word $0x8948; byte $0xe5  // movq         %rsp, %rbp
	movl si, ax
	imul3q $1374389535, ax, ax
	shrq $37, ax
	leaq 0(ax)(ax*1), dx
	word $0xc06b; byte $0x64  // imull        $100, �x, �x
	movl si, cx
	subl ax, cx
	addq cx, cx
	cmpl si, $1000
	jb lbb1_2
	long $0x60058d48; word $0x0000; byte $0x00  // leaq         $96(%rip), %rax  /* _digits(%rip) */
	movb 0(dx)(ax*1), ax
	movb ax, 0(di)
	movl $1, ax
	jmp lbb1_3

然后在 go 中调用该函数:

func test___u32toa_small(t *testing.t) {
 var buf [32]byte
 type args struct {
  out *byte
  val uint32
 }
 tests := []struct {
  name    string
  args    args
  wantret int
 }{
  {
   name: "9999",
   args: args{
    out: &buf[0],
    val: 9999,
   },
   wantret: 4,
  },
  {
   name: "1234",
   args: args{
    out: &buf[0],
    val: 1234,
   },
   wantret: 4,
  },
 }
 for _, tt := range tests {
  t.run(tt.name, func(t *testing.t) {
   got := __u32toa_small(tt.args.out, tt.args.val)
   assert.equalf(t, tt.wantret, got, "__u32toa_small(%v, %v)", tt.args.out, tt.args.val)
   assert.equalf(t, tt.name, string(buf[:tt.wantret]), "ret string must equal name")
  })
 }
}

测试成功,__u32toa_small 函数不仅成功运行,而且通过了测试。

最后,我们来做一个性能跑分看看 __u32toa_small 和 strconv.itoa 之间的性能差异:

func benchmarkgoconv(b *testing.b) {
 val := int(rand.int31() % 10000)
 b.resettimer()
 for n := 0; n < b.n; n   {
  strconv.itoa(val)
 }
}
func benchmarkfastconv(b *testing.b) {
 var buf [32]byte
 val := uint32(rand.int31() % 10000)
 b.resettimer()
 for n := 0; n < b.n; n   {
  __u32toa_small(&buf[0], val)
 }
}

使用 go test -bench 运行这两个性能测试函数,结果如下:

benchmarkgoconv
benchmarkgoconv-12     60740782         19.52 ns/op
benchmarkfastconv
benchmarkfastconv-12    122945924          9.455 ns/op

从结果中,可以明显看出 __u32toa_small 优于 itoa,大概有一倍的提升。

至此,go 的两个黑魔法技巧已经介绍完毕了,感兴趣的同学可以自己实践看看。

go 的黑魔法一定程度上都使用了 unsafe 的能力,这也是 go 不提倡的,当然使用 unsafe 其实就和普通的 c 代码编写一样,因此也无需有太强的心理负担。

实际上,上述的两种方法都被 sonic 用在了生产环境上,而且带来的很大的性能提升,节约大量资源。

因此,当 go 现有的标准库无法满足你的需求时,不要受到语言本身的限制,而是用虽然少见但有效的方式去解决
它。

希望上面的两个黑魔法能带你对 go 不一样的认识。

js555888金沙老品牌的版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由思创斯整理,转载请注明出处:https://ispacesoft.com/122054.html

(0)

相关推荐

  • (永久)激活码无限使用_2021年激活码刚出无限使用不过期(goland2022.01 激活码)2021最新分享一个能用的的激活码出来,希望能帮到需要激活的朋友。目前这个是能用的,但是用的人多了之后也会失效,会不定时更新的,大家持续关注此网站~intell…

  • 官方给的永久激活码2021_未过期的激活码大全(goland激活码最新)jetbrains旗下有多款编译器工具(如:intellij、webstorm、pycharm等)在各编程领域几乎都占据了垄断地位。建立在开源intellij平台之上,过去…

  • 虚拟人声合成软件_mg16混响在直播、语聊房、k 歌房场景中,为增加趣味性和互动性,玩家可以通过变声来搞怪,通过混响烘托气氛,通过立体声使声音更具立体感。zegoexpress sdk 提供了多种预设的变声、混响、混响回声、立体声

  • google搜索引擎怎么用_常用灭火方法这是一篇整理其他资料集成的详细使用方法,留给自己看看也分享给大家,其实有很多人在很早前或许都已经知道这些方法了,我只是在这里重新搬运一下,原文请看:http://blog.sina.com.cn/s/blog_5ac960fd0101it0h.htmlgoogle查找更多资源技巧搜索google大家都用过吧?我们正是利用它强劲的搜索功能来突破封锁下载,google搜索和

  • django数据库索引_外键约束名称取消外键约束解决此类报错:可以再开发环境中暂时取消外键约束,生产环境在放开在settings.py文件数据库的配置上加上此段代码即可databases = { ‘default’: { ‘engine’: ‘django.db.backends.mysql’, ‘name’: db_name, ‘user’: db_user, ‘password’: db_password, ‘host’: db_host,

  • 硬盘4k对齐什么意思不对齐会怎么样_移动硬盘4k对齐方法

    硬盘4k对齐什么意思不对齐会怎么样_移动硬盘4k对齐方法本来在csdn下载这个工具,发现要7分,好吧,下了。谁知道是个坑比,没分,最后下载一个免费的,有时候免费的更好。其实这些我都知道有什么作用,只是看到网上说,其实机械硬盘4k对齐也会提高硬盘性能,并且给出了相应测试数据。百度搜索可以使用paragonalignmenttool工具进行无损对齐,按照方法试了,发现并不成功,出现如下图:金沙1005的解决方案:1、下载分区助手并安装打开2

  • django_session表_cookie session localstorage 区别在django中session是通过一个中间件管理的。如果要在应用程序中使用session,需要在settings.py中的middleware_classes变量中加入’django.contrib.sessions.middleware.sessionmiddleware’ 。django中的session有3种存储方式:放在数据库、缓存或者文件系统中,下面分别予以介绍。1. 将session存储在数据库中:如果要将session存储在数据库中,我们需要将 ’django.contrib.sess

  • golang waitgroup源码解析[亲测有效]go waitgroup源码解析 结构体 type waitgroup struct { nocopy nocopy state1 [3]uint32 } 其中state1代表三个字段:counter

发表回复

您的电子邮箱地址不会被公开。

联系金沙1005

关注“java架构师必看”公众号

回复4,添加站长微信。

附言:ispacesoft.com网而来。

关注微信
网站地图