Go 高性能编程技法

Posted 腾讯技术工程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go 高性能编程技法相关的知识,希望对你有一定的参考价值。

作者:dablelv,腾讯 IEGggG 后台开发工程师

代码的稳健、可读和高效是我们每一个 coder 的共同追求。本文将结合 Go 语言特性,为书写效率更高的代码,从常用数据结构、内存管理和并发,三个方面给出相关建议。话不多说,让我们一起学习 Go 高性能编程的技法吧。

能够接收不同类型的入参,通过格式化输出完成字符串的拼接,使用非常方便。但因其底层实现使用了反射,性能上会有所损耗。

运算符 + 只能简单地完成字符串之间的拼接,非字符串类型的变量需要单独做类型转换。行内拼接字符串不会产生内存分配,也不涉及类型地动态转换,所以性能上优于fmt.Sprintf()

从性能出发,兼顾易用可读,如果待拼接的变量不涉及类型转换且数量较少(<=5),行内拼接字符串推荐使用运算符 +,反之使用 fmt.Sprintf()

下面看下二者的性能对比。

的性能相近。如果结果字符串的长度是可预知的,使用 byte[] 且预先分配容量的拼接方式性能最佳。

所以如果对性能要求非常严格,或待拼接的字符串数量足够多时,建议使用  byte[] 预先分配容量这种方式。

综合易用性和性能,一般推荐使用strings.Builder来拼接字符串。

string.Builder也提供了预分配内存的方式 Grow:

函数用于生成指定长度元素类型为 int 的切片。从最终的结果可以看到,遍历 []int 类型的切片,下标与 range 遍历性能几乎没有区别。

用于获取变量的对齐系数。对齐系数决定了字段的偏移和变量的大小,两者必须是对齐系数的整数倍。

约为 20 亿个,每个协程内部几乎没有做什么事情。正常的情况下呢,这个程序会乱序输出 0 ~ 2^31-1 个数字。

程序会像预期的那样顺利的运行吗?

go run main.go
...
108668
1142025
panic: too many concurrent operations on a single file or socket (max 1048575)

goroutine 1158408 [running]:
internal/poll.(*fdMutex).rwlock(0xc0000ae060, 0x0)
        /usr/local/go/src/internal/poll/fd_mutex.go:147 +0x11b
internal/poll.(*FD).writeLock(...)
        /usr/local/go/src/internal/poll/fd_mutex.go:239
internal/poll.(*FD).Write(0xc0000ae060, 0xc12cadf690, 0x8, 0x8)
        /usr/local/go/src/internal/poll/fd_unix.go:262 +0x72
os.(*File).write(...)
        /usr/local/go/src/os/file_posix.go:49
os.(*File).Write(0xc0000ac008, 0xc12cadf690, 0x1, 0xc12ea62f50)
        /usr/local/go/src/os/file.go:176 +0x65
fmt.Fprintln(0x10c00e0, 0xc0000ac008, 0xc12ea62f90, 0x1, 0x1)
        /usr/local/go/src/fmt/print.go:265 +0x75
fmt.Println(...)
        /usr/local/go/src/fmt/print.go:274
main.main.func1(0x0)
        /Users/dablelv/work/code/test/main.go:16 +0x8f
...

运行的结果是程序直接崩溃了,关键的报错信息是:

panic: too many concurrent operations on a single file or socket (max 1048575)

对单个 file/socket 的并发操作个数超过了系统上限,这个报错是 fmt.Printf 函数引起的,fmt.Printf 将格式化后的字符串打印到屏幕,即标准输出。在 Linux 系统中,标准输出也可以视为文件,内核(Kernel)利用文件描述符(File Descriptor)来访问文件,标准输出的文件描述符为 1,错误输出文件描述符为 2,标准输入的文件描述符为 0。

简而言之,系统的资源被耗尽了。

那如果我们将 fmt.Printf 这行代码去掉呢?那程序很可能会因为内存不足而崩溃。这一点更好理解,每个协程至少需要消耗 2KB 的空间,那么假设计算机的内存是 4GB,那么至多允许 4GB/2KB = 1M 个协程同时存在。那如果协程中还存在着其他需要分配内存的操作,那么允许并发执行的协程将会数量级地减少。

2.1.2 协程的代价

前面的例子过于极端,一般情况下程序也不会无限开辟协程,旨在说明协程数量是有限制的,不能无限开辟。

如果我们开辟很多协程,但不会导致程序崩溃,可以吗?如果真要这么做的话,我们应该清楚地知道,协程虽然轻量,但仍有开销。

Go 的开销主要是三个方面:创建(占用内存)、调度(增加调度器负担)和删除(增加 GC 压力)。

  • 内存开销
  • 空间上,一个 Go 程占用约 2K 的内存,在源码 src/runtime/runtime2.go里面,我们可以找到 Go 程的结构定义type g struct。

  • 调度开销
  • 时间上,协程调度也会有 CPU 开销。我们可以利用runntime.Gosched()让当前协程主动让出 CPU 去执行另外一个协程,下面看一下协程之间切换的耗时。

    const NUM = 10000

    func cal() 
     for i := 0; i < NUM; i++ 
      runtime.Gosched()
     


    func main() 
     // 只设置一个 Processor
     runtime.GOMAXPROCS(1)
     start := time.Now().UnixNano()
     go cal()
     for i := 0; i < NUM; i++ 
      runtime.Gosched()
     
     end := time.Now().UnixNano()
     fmt.Printf("total %vns per %vns", end-start, (end-start)/NUM)

    运行输出:

    total 997200ns per 99ns

    可见一次协程的切换,耗时大概在 100ns,相对于线程的微秒级耗时切换,性能表现非常优秀,但是仍有开销。

  • GC 开销 创建 Go 程到运行结束,占用的内存资源是需要由 GC 来回收,如果无休止地创建大量 Go 程后,势必会造成对 GC 的压力。
  • package main

    import (
     "fmt"
     "runtime"
     "runtime/debug"
     "sync"
     "time"
    )

    func createLargeNumGoroutine(num int, wg *sync.WaitGroup) 
     wg.Add(num)
     for i := 0; i < num; i++ 
      go func() 
       defer wg.Done()
      ()
     


    func main() 
     // 只设置一个 Processor 保证 Go 程串行执行
     runtime.GOMAXPROCS(1)
     // 关闭GC改为手动执行
     debug.SetGCPercent(-1)

     var wg sync.WaitGroup
     createLargeNumGoroutine(1000, &wg)
     wg.Wait()
     t := time.Now()
     runtime.GC() // 手动GC
     cost := time.Since(t)
     fmt.Printf("GC cost %v when goroutine num is %v\\n", cost, 1000)

     createLargeNumGoroutine(10000, &wg)
     wg.Wait()
     t = time.Now()
     runtime.GC() // 手动GC
     cost = time.Since(t)
     fmt.Printf("GC cost %v when goroutine num is %v\\n", cost, 10000)

     createLargeNumGoroutine(100000, &wg)
     wg.Wait()
     t = time.Now()
     runtime.GC() // 手动GC
     cost = time.Since(t)
     fmt.Printf("GC cost %v when goroutine num is %v\\n", cost, 100000)

    运行输出:

    GC cost 0s when goroutine num is 1000
    GC cost 2.0027ms when goroutine num is 10000
    GC cost 30.9523ms when goroutine num is 100000

    当创建的 Go 程数量越多,GC 耗时越大。

    上面的分析目的是为了尽可能地量化 Goroutine 的开销。虽然官方宣称用 Golang 写并发程序的时候随便起个成千上万的 Goroutine 毫无压力,但当我们起十万、百万甚至千万个 Goroutine 呢?Goroutine 轻量的开销将被放大。

    2.2 限制协程数量

    系统地资源是有限,协程是有代价的,为了保护程序,提高性能,我们应主动限制并发的协程数量。

    可以利用信道 channel 的缓冲区大小来实现。

    func main() 
     var wg sync.WaitGroup
     ch := make(chan struct3)
     for i := 0; i < 10; i++ 
      ch <- struct
      wg.Add(1)
      go func(i int) 
       defer wg.Done()
       log.Println(i)
       time.Sleep(time.Second)
       <-ch
      (i)
     
     wg.Wait()

    上例中创建了缓冲区大小为 3 的 channel,在没有被接收的情况下,至多发送 3 个消息则被阻塞。开启协程前,调用ch <- struct,若缓存区满,则阻塞。协程任务结束,调用 <-ch 释放缓冲区。

    sync.WaitGroup 并不是必须的,例如 Http 服务,每个请求天然是并发的,此时使用 channel 控制并发处理的任务数量,就不需要 sync.WaitGroup。

    运行结果如下:

    2022/03/06 20:37:02 0
    2022/03/06 20:37:02 2
    2022/03/06 20:37:02 1
    2022/03/06 20:37:03 3
    2022/03/06 20:37:03 4
    2022/03/06 20:37:03 5
    2022/03/06 20:37:04 6
    2022/03/06 20:37:04 7
    2022/03/06 20:37:04 8
    2022/03/06 20:37:05 9

    从日志中可以很容易看到,每秒钟只并发执行了 3 个任务,达到了协程并发控制的目的。

    2.3 协程池化

    上面的例子只是简单地限制了协程开辟的数量。在此基础之上,基于对象复用的思想,我们可以重复利用已开辟的协程,避免协程的重复创建销毁,达到池化的效果。

    协程池化,我们可以自己写一个协程池,但不推荐这么做。因为已经有成熟的开源库可供使用,无需再重复造轮子。目前有很多第三方库实现了协程池,可以很方便地用来控制协程的并发数量,比较受欢迎的有:

  • Jeffail/tunny
  • panjf2000/ants
  • 下面以 panjf2000/ants 为例,简单介绍其使用。

    ants 是一个简单易用的高性能 Goroutine 池,实现了对大规模 Goroutine 的调度管理和复用,允许使用者在开发并发程序的时候限制 Goroutine 数量,复用协程,达到更高效执行任务的效果。

    package main

    import (
     "fmt"
     "time"

     "github.com/panjf2000/ants"
    )

    func main() 
     // Use the common pool
     for i := 0; i < 10; i++ 
      i := i
      ants.Submit(func() 
       fmt.Println(i)
      )
     
     time.Sleep(time.Second)

    使用 ants,我们简单地使用其默认的协程池,直接将任务提交并发执行。默认协程池的缺省容量 math.MaxInt32。

    如果自定义协程池容量大小,可以调用 NewPool 方法来实例化具有给定容量的池,如下所示:

    // Set 10000 the size of goroutine pool
    p, _ := ants.NewPool(10000)
    2.4 小结

    Golang 为并发而生。Goroutine 是由 Go 运行时管理的轻量级线程,通过它我们可以轻松实现并发编程。Go 虽然轻量,但天下没有免费的午餐,无休止地开辟大量 Go 程势必会带来性能影响,甚至程序崩溃。所以,我们应尽可能的控制协程数量,如果有需要,请复用它。

    3.使用 sync.Once 避免重复执行

    3.1 简介

    sync.Once 是 Go 标准库提供的使函数只执行一次的实现,常应用于单例模式,例如初始化配置、保持数据库连接等。作用与 init 函数类似,但有区别。

  • init 函数是当所在的 package 首次被加载时执行,若迟迟未被使用,则既浪费了内存,又延长了程序加载时间。
  • sync.Once 可以在代码的任意位置初始化和调用,因此可以延迟到使用时再执行,并发场景下是线程安全的。
  • 在多数情况下,sync.Once 被用于控制变量的初始化,这个变量的读写满足如下三个条件:

  • 当且仅当第一次访问某个变量时,进行初始化(写);
  • 变量初始化过程中,所有读都被阻塞,直到初始化完成;
  • 变量仅初始化一次,初始化完成后驻留在内存里。
  • 3.2 原理

    sync.Once 用来保证函数只执行一次。要达到这个效果,需要做到两点:

  • 计数器,统计函数执行次数;
  • 线程安全,保障在多 Go 程的情况下,函数仍然只执行一次,比如锁。
  • 3.2.1 源码

    下面看一下 sync.Once 结构,其有两个变量。使用 done 统计函数执行次数,使用锁 m 实现线程安全。果不其然,和上面的猜想一致。

    // Once is an object that will perform exactly one action.
    //
    // A Once must not be copied after first use.
    type Once struct 
     // done indicates whether the action has been performed.
     // It is first in the struct because it is used in the hot path.
     // The hot path is inlined at every call site.
     // Placing done first allows more compact instructions on some architectures (amd64/386),
     // and fewer instructions (to calculate offset) on other architectures.
     done uint32
     m    Mutex

    sync.Once 仅提供了一个导出方法 Do(),参数 f 是只会被执行一次的函数,一般为对象初始化函数。

    // go version go1.17 darwin/amd64

    // Do calls the function f if and only if Do is being called for the
    // first time for this instance of Once. In other words, given
    //  var once Once
    // if once.Do(f) is called multiple times, only the first call will invoke f,
    // even if f has a different value in each invocation. A new instance of
    // Once is required for each function to execute.
    //
    // Do is intended for initialization that must be run exactly once. Since f
    // is niladic, it may be necessary to use a function literal to capture the
    // arguments to a function to be invoked by Do:
    //  config.once.Do(func()  config.init(filename) )
    //
    // Because no call to Do returns until the one call to f returns, if f causes
    // Do to be called, it will deadlock.
    //
    // If f panics, Do considers it to have returned; future calls of Do return
    // without calling f.
    //
    func (o *Once) Do(f func()) 
     // Note: Here is an incorrect implementation of Do:
     //
     // if atomic.CompareAndSwapUint32(&o.done, 0, 1) 
     //  f()
     // 
     //
     // Do guarantees that when it returns, f has finished.
     // This implementation would not implement that guarantee:
     // given two simultaneous calls, the winner of the cas would
     // call f, and the second would return immediately, without
     // waiting for the first\'s call to f to complete.
     // This is why the slow path falls back to a mutex, and why
     // the atomic.StoreUint32 must be delayed until after f returns.

     if atomic.LoadUint32(&o.done) == 0 
      // Outlined slow-path to allow inlining of the fast-path.
      o.doSlow(f)
     


    func (o *Once) doSlow(f func()) 
     o.m.Lock()
     defer o.m.Unlock()
     if o.done == 0 
      defer atomic.StoreUint32(&o.done, 1)
      f()
     

    抛去大段的注释,可以看到 sync.Once 实现非常简洁。Do() 函数中,通过对成员变量 done 的判断,来决定是否执行传入的任务函数。执行任务函数前,通过锁保证任务函数的执行和 done 的修改是一个互斥操作。在执行任务函数前,对 done 做一个二次判断,来保证任务函数只会被执行一次,done 只会被修改一次。

    3.2.2  done 为什么是第一个字段

    从字段 done 前有一段注释,说明了done 为什么是第一个字段。

    done 在热路径中,done 放在第一个字段,能够减少 CPU 指令,也就是说,这样做能够提升性能。

    热路径(hot path)是程序非常频繁执行的一系列指令,sync.Once 绝大部分场景都会访问 o.done,在热路径上是比较好理解的。如果 hot path 编译后的机器码指令更少,更直接,必然是能够提升性能的。

    为什么放在第一个字段就能够减少指令呢?因为结构体第一个字段的地址和结构体的指针是相同的,如果是第一个字段,直接对结构体的指针解引用即可。如果是其他的字段,除了结构体指针外,还需要计算与第一个值的偏移(calculate offset)。在机器码中,偏移量是随指令传递的附加值,CPU 需要做一次偏移值与指针的加法运算,才能获取要访问的值的地址。因为,访问第一个字段的机器代码更紧凑,速度更快。

    参考 What does “hot path” mean in the context of sync.Once? - StackOverflow

    3.3 性能差异

    我们以一个简单示例,来说明使用 sync.Once 保证函数只会被执行一次和多次执行,二者的性能差异。

    考虑一个简单的场景,函数 ReadConfig 需要读取环境变量,并转换为对应的配置。环境变量在程序执行前已经确定,执行过程中不会发生改变。ReadConfig 可能会被多个协程并发调用,为了提升性能(减少执行时间和内存占用),使用 sync.Once 是一个比较好的方式。

    type Config struct 
     GoRoot string
     GoPath string


    var (
     once   sync.Once
     config *Config
    )

    func ReadConfigWithOnce() *Config 
     once.Do(func() 
      config = &Config
       GoRoot: os.Getenv("GOROOT"),
       GoPath: os.Getenv("GOPATH"),
      
     )
     return config


    func ReadConfig() *Config 
     return &Config
      GoRoot: os.Getenv("GOROOT"),
      GoPath: os.Getenv("GOPATH"),
     

    我们看下二者的性能差异。

    func BenchmarkReadConfigWithOnce(b *testing.B) 
     for i := 0; i < b.N; i++ 
      _ = ReadConfigWithOnce()
     


    func BenchmarkReadConfig(b *testing.B) 
     for i := 0; i < b.N; i++ 
      _ = ReadConfig()
     

    执行测试结果如下:

    go test -bench=. main/once
    goos: darwin
    goarch: amd64
    pkg: main/once
    cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
    BenchmarkReadConfigWithOnce-12          670438965                1.732 ns/op
    BenchmarkReadConfig-12                  13339154                87.46 ns/op
    PASS
    ok      main/once       3.006s

    sync.Once 中保证了 Config 初始化函数仅执行了一次,避免了多次重复初始化,在并发环境下很有用。

    4.使用 sync.Cond 通知协程

    4.1 简介

    sync.Cond 是基于互斥锁/读写锁实现的条件变量,用来协调想要访问共享资源的那些 Goroutine,当共享资源的状态发生变化的时候,sync.Cond 可以用来通知等待条件发生而阻塞的 Goroutine。

    sync.Cond 基于互斥锁/读写锁,它和互斥锁的区别是什么呢?

    互斥锁 sync.Mutex 通常用来保护共享的临界资源,条件变量 sync.Cond 用来协调想要访问共享资源的 Goroutine。当共享资源的状态发生变化时,sync.Cond 可以用来通知被阻塞的 Goroutine。

    4.2 使用场景

    sync.Cond 经常用在多个 Goroutine 等待,一个 Goroutine 通知(事件发生)的场景。如果是一个通知,一个等待,使用互斥锁或 channel 就能搞定了。

    我们想象一个非常简单的场景:

    有一个协程在异步地接收数据,剩下的多个协程必须等待这个协程接收完数据,才能读取到正确的数据。在这种情况下,如果单纯使用 chan 或互斥锁,那么只能有一个协程可以等待,并读取到数据,没办法通知其他的协程也读取数据。

    这个时候,就需要有个全局的变量来标志第一个协程数据是否接受完毕,剩下的协程,反复检查该变量的值,直到满足要求。或者创建多个 channel,每个协程阻塞在一个 channel 上,由接收数据的协程在数据接收完毕后,逐个通知。总之,需要额外的复杂度来完成这件事。

    Go 语言在标准库 sync 中内置一个 sync.Cond 用来解决这类问题。

    4.3 原理

    sync.Cond 内部维护了一个等待队列,队列中存放的是所有在等待这个 sync.Cond 的 Go 程,即保存了一个通知列表。sync.Cond 可以用来唤醒一个或所有因等待条件变量而阻塞的 Go 程,以此来实现多个 Go 程间的同步。

    sync.Cond 的定义如下:

    // Cond implements a condition variable, a rendezvous point
    // for goroutines waiting for or announcing the occurrence
    // of an event.
    //
    // Each Cond has an associated Locker L (often a *Mutex or *RWMutex),
    // which must be held when changing the condition and
    // when calling the Wait method.
    //
    // A Cond must not be copied after first use.
    type Cond struct 
     noCopy noCopy

     // L is held while observing or changing the condition
     L Locker

     notify  notifyList
     checker copyChecker

    每个 Cond 实例都会关联一个锁 L(互斥锁 *Mutex,或读写锁 *RWMutex),当修改条件或者调用 Wait 方法时,必须加锁。

    sync.Cond 的四个成员函数定义如下:

    // NewCond returns a new Cond with Locker l.
    func NewCond(l Locker) *Cond 
     return &CondL: l

    NewCond 创建 Cond 实例时,需要关联一个锁。

    // Wait atomically unlocks c.L and suspends execution
    // of the calling goroutine. After later resuming execution,
    // Wait locks c.L before returning. Unlike in other systems,
    // Wait cannot return unless awoken by Broadcast or Signal.
    //
    // Because c.L is not locked when Wait first resumes, the caller
    // typically cannot assume that the condition is true when
    // Wait returns. Instead, the caller should Wait in a loop:
    //
    //    c.L.Lock()
    //    for !condition() 
    //        c.Wait()
    //    
    //    ... make use of condition ...
    //    c.L.Unlock()
    //
    func (c *Cond) Wait() 
     c.checker.check()
     t := runtime_notifyListAdd(&c.notify)
     c.L.Unlock()
     runtime_notifyListWait(&c.notify, t)
     c.L.Lock()

    Wait 用于阻塞调用者,等待通知。调用 Wait 会自动释放锁 c.L,并挂起调用者所在的 goroutine。如果其他协程调用了 Signal 或 Broadcast 唤醒了该协程,那么 Wait 方法在结束阻塞时,会重新给 c.L 加锁,并且继续执行 Wait 后面的代码。

    对条件的检查,使用了 for !condition() 而非 if,是因为当前协程被唤醒时,条件不一定符合要求,需要再次 Wait 等待下次被唤醒。为了保险起,使用 for 能够确保条件符合要求后,再执行后续的代码。

    // Signal wakes one goroutine waiting on c, if there is any.
    //
    // It is allowed but not required for the caller to hold c.L
    // during the call.
    func (c *Cond) Signal() 
     c.checker.check()
     runtime_notifyListNotifyOne(&c.notify)


    // Broadcast wakes all goroutines waiting on c.
    //
    // It is allowed but not required for the caller to hold c.L
    // during the call.
    func (c *Cond) Broadcast() 
     c.checker.check()
     runtime_notifyListNotifyAll(&c.notify)

    Signal 只唤醒任意 1 个等待条件变量 c 的 goroutine,无需锁保护。Broadcast 唤醒所有等待条件变量 c 的 goroutine,无需锁保护。

    4.4 使用示例

    我们实现一个简单的例子,三个协程调用 Wait() 等待,另一个协程调用 Broadcast() 唤醒所有等待的协程。

    var done = false

    func read(name string, c *sync.Cond) 
     c.L.Lock()
     for !done 
      c.Wait()
     
     log.Println(name, "starts reading")
     c.L.Unlock()


    func write(name string, c *sync.Cond) 
     log.Println(name, "starts writing")
     time.Sleep(time.Second)
     done = true
     log.Println(name, "wakes all")
     c.Broadcast()


    func main() 
     cond := sync.NewCond(&sync.Mutex)

     go read("reader1", cond)
     go read("reader2", cond)
     go read("reader3", cond)
     write("writer", cond)

     time.Sleep(time.Second * 3)

  • done 即多个 Goroutine 阻塞等待的条件。
  • read() 调用 Wait() 等待通知,直到 done 为 true。
  • write() 接收数据,接收完成后,将 done 置为 true,调用 Broadcast() 通知所有等待的协程。
  • write() 中的暂停了 1s,一方面是模拟耗时,另一方面是确保前面的 3 个 read 协程都执行到 Wait(),处于等待状态。main 函数最后暂停了 3s,确保所有操作执行完毕。
  • 运行输出:

    go run main.go
    2022/03/07 17:20:09 writer starts writing
    2022/03/07 17:20:10 writer wakes all
    2022/03/07 17:20:10 reader3 starts reading
    2022/03/07 17:20:10 reader1 starts reading
    2022/03/07 17:20:10 reader2 starts reading

    更多关于 sync.Cond 的讨论可参考 How to correctly use sync.Cond? - StackOverflow。

    4.5 注意事项
  • sync.Cond 不能被复制
  • sync.Cond 不能被复制的原因,并不是因为其内部嵌套了 Locker。因为 NewCond 时传入的 Mutex/RWMutex 指针,对于 Mutex 指针复制是没有问题的。

    主要原因是 sync.Cond 内部是维护着一个 Goroutine 通知队列 notifyList。如果这个队列被复制的话,那么就在并发场景下导致不同 Goroutine 之间操作的 notifyList.wait、notifyList.notify 并不是同一个,这会导致出现有些 Goroutine 会一直阻塞。

  • 唤醒顺序
  • 从等待队列中按照顺序唤醒,先进入等待队列,先被唤醒。

  • 调用 Wait() 前要加锁
  • 调用 Wait() 函数前,需要先获得条件变量的成员锁,原因是需要互斥地变更条件变量的等待队列。在 Wait() 返回前,会重新上锁。

    参考文献

  • github.com/uber-go/guide
  • go-proverbs
  • github/dgryski/go-perfbook
  • High Performance Go Workshop - Dave Cheney
  • atomic 的原理与使用场景
  • 极客兔兔.Go 语言高性能编程
  • 深度解密Go 语言之sync.Pool - Stefno - 博客园

  • 最近好文:
    在鹅厂工作1到11年的程序媛
    技术她力量,鹅厂女博士的寻“豹”之旅
    微信全文搜索技术优化

    traits编程技法

      看了《stl源码剖析》中关于traits的部分,由于对模板还不是很熟悉,就看了一下还未完工的C++ Template 进阶指南 ,感觉收获很大,推荐一下。

      在使用迭代器时,为了知道它的相应类型,可以使用模板的参数推导,代码如下

    template <class T>
    struct MyIter
    typedef T value_type; 
    T* ptr;
    MyIter(T* p=0) : ptr(p) { }
    T& operator*() const { return *ptr; }
    // ...
    };
    template <class I>
    typename I::value_type // 返回类型
    func(I ite)
    { return *ite; }
    // ...
    MyIter<int> ite(new int(8));
    cout << func(ite); // 输出:8

     用typename是因为I::value_type要在模板实例化的时候才能知道是不是一个类型,所以用typename声明为类型。具体可见我前面提到的那篇文章,说得很详细。

      但是这样有一个问题,有些迭代器(比如指针)并不是一个类,也就用不了这个办法了。这时就要用traits思想把相应的类型萃取出来。

    原始的类模板如下:

    template <class I>
    struct iterator_traits { 
    typedef typename I::value_type value_type;
    };

    偏特化:

    template <class T>
    struct iterator_traits<T*> { 
    typedef T value_type;
    };

     这样就得到了相应的类型,同理,对于const T*有

    template <class T>
    struct iterator_traits<const T*> { 
    typedef T value_type;
    };

     有了这个技巧,stl就可以根据需要制定提取迭代器相应类型的办法,比如对于下面五种有不同的方式提取类型

    value_type
    difference_type
    pointerpointer
    referencereference
    iteratoriterator_category

    具体方式书中有说,就不赘述了。

     

    以上是关于Go 高性能编程技法的主要内容,如果未能解决你的问题,请参考以下文章

    Go 高性能编程技法

    Go 高性能编程技法

    Go 高性能编程技法

    Go 高性能编程技法

    traits编程技法

    Go语言并发编程