【问题标题】:Why Go use cgo on Windows for a simple File.Write?为什么 Go 在 Windows 上使用 cgo 进行简单的 File.Write?
【发布时间】:2016-06-18 09:44:37
【问题描述】:

从 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

【问题讨论】:

  • 我记得在某处读到过,Windows 操作系统没有像 unix 系统那样的系统调用接口,而是公开了一个 C API。不知道那是多么真实。
  • 我可能是错的,但看看github.com/golang/go/blob/master/src/syscall/…,似乎所有的系统调用都通过cgo,但我对windows并不熟悉。这个问题更适合 golangnuts ML。
  • 事实上Windows确实有系统调用。问题在于,与某些其他操作系统内核(包括 Linux)相反,它们具有稳定的系统调用编号表,只能通过添加新的系统调用来扩展,Windows 从未发布这些编号,并且它们确实在不同的该系列操作系统的内核版本。它们甚至可能在不同的服务包之间存在差异。由于访问这些系统调用的唯一记录方式是通过 DLL,这就是 Go 应该做的事情。
  • 请注意,您观察到的是存在 bufio 包的原因之一:系统调用在任何具有特权进程分离的操作系统中都是昂贵的,因为执行上下文必须是“门控”从用户空间到内核空间并返回。如果您使用使用 32KiB 缓冲区 IIRC 的 io.Copy(),我认为结果会好很多。
  • @OneOfOne:“我对 Windows 不是很熟悉。这个问题更适合 golangnuts。”没有必要在 golang-nuts 上提问。 Stack Overflow 上有很多 Windows 专家。

标签: linux windows go cgo


【解决方案1】:

Go 不执行文件 I/O,它将任务委托给操作系统。请参阅依赖于 Go 操作系统的 syscall 包。

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

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

Command cgo

Passing pointers

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

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

Go 代码可以将 Go 指针传递给 C,前提是它指向的 Go 内存 points 不包含任何 Go 指针。 C 代码必须保留这个 属性:它不能在 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。指针处理的完整检查,在 一些运行时间成本,可通过 GODEBUG=cgocheck=2 获得。

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

“这些规则在运行时动态检查。”


基准测试:

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

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

至少连续运行 3 次基准测试。操作系统试图变得聪明。例如,缓存 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

【讨论】:

  • 谢谢。那么runtime.cgocall 不代表它使用cgo。但它做了类似的工作。
  • @samonzeweb:runtime.cgocall 时间测量通过 cgo 调用时在 Go 之外花费的时间,以及在 Windows 上调用 Windows dlls 时花费的时间。在您的 disk.go 程序中,Go 之外的所有调用都是针对 Windows dlls。
  • @samonzeweb:请参阅我对一些 cmets 基准测试和一些基准测试结果的修订答案。
  • 另外,如果这回答了你的问题,请接受@samonzeweb
  • 正如我自己所说,我没有比较 Windows 和 Linux 之间的性能,因为它不是相同的硬件。我想看看对runtime.cgocall 的调用是否特定于Windows,然后我问了关于这种差异的问题。这不是性能问题,而是实施问题。我有我的答案,谢谢。即使 Linux 和 Windows 之间的基准测试不是目的,也欢迎您的基准测试,我迟早会结束它;)
猜你喜欢
  • 2020-11-12
  • 2015-11-25
  • 1970-01-01
  • 1970-01-01
  • 2011-04-19
  • 2019-03-29
  • 1970-01-01
  • 2020-02-21
  • 2016-10-05
相关资源
最近更新 更多