为什么Go在Windows上使用cgo来获取简单的File.Write?

时间:2016-03-04 15:26:09

标签: linux windows go cgo

将一个简单的程序从C#重写为Go,我发现生成的可执行文件慢了3到4倍。特别是Go版本使用3到4倍的CPU。这是令人惊讶的,因为代码执行许多I / O并且不应该消耗大量的CPU。

我制作了一个非常简单的版本,只进行顺序写入,并制作了基准测试。我在Windows 10和Linux(Debian Jessie)上运行相同的基准测试。时间无法比较(不是相同的系统,磁盘......)但结果很有趣。

我在两个平台上都使用相同的Go版本:1.6

在Windows上,os.File.Write使用cgo(参见下面的runtime.cgocall),而不是Linux。为什么?

这是disk.go程序:

    package main

    import (
        "crypto/rand"
        "fmt"
        "os"
        "time"
    )

    const (
        // size of the test file
        fullSize = 268435456
        // size of read/write per call
        partSize = 128
        // path of temporary test file
        filePath = "./bigfile.tmp"
    )

    func main() {
        buffer := make([]byte, partSize)

        seqWrite := func() error {
            return sequentialWrite(filePath, fullSize, buffer)
        }

        err := fillBuffer(buffer)
        panicIfError(err)
        duration, err := durationOf(seqWrite)
        panicIfError(err)
        fmt.Printf("Duration : %v\n", duration)
    }

    // It's just a test ;)
    func panicIfError(err error) {
        if err != nil {
            panic(err)
        }
    }

    func durationOf(f func() error) (time.Duration, error) {
        startTime := time.Now()
        err := f()
        return time.Since(startTime), err
    }

    func fillBuffer(buffer []byte) error {
        _, err := rand.Read(buffer)
        return err
    }

    func sequentialWrite(filePath string, fullSize int, buffer []byte) error {
        desc, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
        if err != nil {
            return err
        }
        defer func() {
            desc.Close()
            err := os.Remove(filePath)
            panicIfError(err)
        }()

        var totalWrote int
        for totalWrote < fullSize {
            wrote, err := desc.Write(buffer)
            totalWrote += wrote
            if err != nil {
                return err
            }
        }

        return nil
    }

基准测试(disk_test.go):

    package main

    import (
        "testing"
    )

    // go test -bench SequentialWrite -cpuprofile=cpu.out
    // Windows : go tool pprof -text -nodecount=10 ./disk.test.exe cpu.out
    // Linux : go tool pprof -text -nodecount=10 ./disk.test cpu.out
    func BenchmarkSequentialWrite(t *testing.B) {
        buffer := make([]byte, partSize)
        err := sequentialWrite(filePath, fullSize, buffer)
        panicIfError(err)
    }

Windows结果(使用cgo):

    11.68s of 11.95s total (97.74%)
    Dropped 18 nodes (cum <= 0.06s)
    Showing top 10 nodes out of 26 (cum >= 0.09s)
          flat  flat%   sum%        cum   cum%
        11.08s 92.72% 92.72%     11.20s 93.72%  runtime.cgocall
         0.11s  0.92% 93.64%      0.11s  0.92%  runtime.deferreturn
         0.09s  0.75% 94.39%     11.45s 95.82%  os.(*File).write
         0.08s  0.67% 95.06%      0.16s  1.34%  runtime.deferproc.func1
         0.07s  0.59% 95.65%      0.07s  0.59%  runtime.newdefer
         0.06s   0.5% 96.15%      0.28s  2.34%  runtime.systemstack
         0.06s   0.5% 96.65%     11.25s 94.14%  syscall.Write
         0.05s  0.42% 97.07%      0.07s  0.59%  runtime.deferproc
         0.04s  0.33% 97.41%     11.49s 96.15%  os.(*File).Write
         0.04s  0.33% 97.74%      0.09s  0.75%  syscall.(*LazyProc).Find

Linux结果(没有cgo):

    5.04s of 5.10s total (98.82%)
    Dropped 5 nodes (cum <= 0.03s)
    Showing top 10 nodes out of 19 (cum >= 0.06s)
          flat  flat%   sum%        cum   cum%
         4.62s 90.59% 90.59%      4.87s 95.49%  syscall.Syscall
         0.09s  1.76% 92.35%      0.09s  1.76%  runtime/internal/atomic.Cas
         0.08s  1.57% 93.92%      0.19s  3.73%  runtime.exitsyscall
         0.06s  1.18% 95.10%      4.98s 97.65%  os.(*File).write
         0.04s  0.78% 95.88%      5.10s   100%  _/home/sam/Provisoire/go-disk.sequentialWrite
         0.04s  0.78% 96.67%      5.05s 99.02%  os.(*File).Write
         0.04s  0.78% 97.45%      0.04s  0.78%  runtime.memclr
         0.03s  0.59% 98.04%      0.08s  1.57%  runtime.exitsyscallfast
         0.02s  0.39% 98.43%      0.03s  0.59%  os.epipecheck
         0.02s  0.39% 98.82%      0.06s  1.18%  runtime.casgstatus

1 个答案:

答案 0 :(得分:6)

Go不执行文件I / O,它将任务委派给操作系统。请参阅Go操作系统相关的syscall包。

Linux和Windows是具有不同OS ABI的不同操作系统。例如,Linux通过syscall.Syscall使用系统调用,Windows使用Windows dll。在Windows上,dll调用是C调用。它没有使用cgo。它确实经过cgoruntime.cgocall使用的相同动态C指针检查。没有runtime.wincall别名。

总之,不同的操作系统具有不同的OS调用机制。

  

Command cgo

     

Passing pointers

     

Go是垃圾收集语言,垃圾收集器需要   知道Go内存的每个指针的位置。因为这,   在Go和C之间传递指针有限制。

     

在本节中,术语Go指针表示指向内存的指针   由Go分配(例如使用&amp;运算符或调用   预定义的新函数)和术语C指针意味着指向   由C分配的内存(例如通过调用C.malloc)。是否一个   指针是Go指针或C指针是动态属性   由内存的分配方式决定;它与此无关   指针的类型。

     

Go代码可以将Go指针传递给C,前提是它的Go内存   points不包含任何Go指针。 C代码必须保留这一点   property:它甚至不能在Go内存中存储任何Go指针   暂时。将指针传递给结构中的字段时,Go   有问题的内存是字段占用的内存,而不是整个内存   结构。将指针传递给数组或切片中的元素时,   有问题的内存是整个阵列或整个后备阵列   切片。

     

在调用返回后,C代码可能无法保留Go指针的副本。

     

C代码调用的Go函数可能不会返回Go指针。一个去   C代码调用的函数可以将C指针作为参数,也可以   通过这些指针存储非指针或C指针数据,但它可能   不将Go指针存储在C指针指向的内存中。一个去   C代码调用的函数可以将Go指针作为参数,但它   必须保留它所指向的Go内存所属的属性   不包含任何Go指针。

     

Go代码可能无法在C内存中存储Go指针。 C代码可以存储Go   C内存中的指针,受上述规则约束:它必须停止存储   C函数返回时的Go指针。

     

在运行时动态检查这些规则。检查是   由GODEBUG环境的cgocheck设置控制   变量。默认设置为GODEBUG = cgocheck = 1,它实现   合理便宜的动态检查。可以完全禁用这些检查   使用GODEBUG = cgocheck = 0。完成对指针处理的检查,at   运行时节省一些费用,可通过GODEBUG = cgocheck = 2获得。

     

可以通过使用不安全的包来破坏这种强制执行,   当然,没有什么能阻止C代码做任何事情   它喜欢。但是,违反这些规则的程序可能会失败   以意想不到的,不可预测的方式。

&#34;这些规则在运行时动态检查。&#34;

基准:

换句话说,有谎言,该死的谎言和基准。

要跨操作系统进行有效比较,您需要在相同的硬件上运行。例如,CPU,内存和生锈或硅磁盘I / O之间的差异。我在同一台机器上双启动Linux和Windows。

背靠背至少运行三次基准测试。操作系统试图变得聪明。例如,缓存I / O.使用虚拟机的语言需要预热时间。等等。

知道你在测量什么。如果您正在执行顺序I / O,则几乎所有时间都花在操作系统上。你关闭了恶意软件保护吗?等等。

等等。

以下是来自使用双启动Windows和Linux的同一台计算机的disk.go的一些结果。

视窗:

>go build disk.go
>/TimeMem disk
Duration : 18.3300322s
Elapsed time   : 18.38
Kernel time    : 13.71 (74.6%)
User time      : 4.62 (25.1%)

Linux的:

$ go build disk.go
$ time ./disk
Duration : 18.54350723s
real    0m18.547s
user    0m2.336s
sys     0m16.236s

实际上,它们是相同的,持续时间为18秒disk.go。操作系统之间的差异只是计算用户时间和内核或系统时间。经历或实时是一样的。

在测试中,内核或系统时间为93.72%runtime.cgocall而不是95.49%syscall.Syscall