为了账号安全,请及时绑定邮箱和手机立即绑定

为什么要在 Windows 上使用 cgo 来实现简单的 File.Write?

为什么要在 Windows 上使用 cgo 来实现简单的 File.Write?

Go
倚天杖 2021-12-20 14:46:03
将一个简单的程序从 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    }
查看完整描述

1 回答

?
浮云间

TA贡献1829条经验 获得超4个赞

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

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

综上所述,不同的操作系统有不同的操作系统调用机制。

命令 cgo

传递指针

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

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

如果 Go 代码指向的 Go 内存不包含任何 Go 指针,则 Go 代码可以将 Go 指针传递给 C。C 代码必须保留这个属性:它不能在 Go 内存中存储任何 Go 指针,即使是临时的。当传递指向结构体中字段的指针时,所讨论的 Go 内存是该字段占用的内存,而不是整个结构体。当传递指向数组或切片中元素的指针时,所讨论的 Go 内存是整个数组或切片的整个后备数组。

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

C 代码调用的 Go 函数可能不会返回 Go 指针。C 代码调用的 Go 函数可能以 C 指针作为参数,它可能通过这些指针存储非指针或 C 指针数据,但它可能不会在 C 指针指向的内存中存储 Go 指针。C 代码调用的 Go 函数可以将 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,您几乎将所有时间都花在操作系统上。您是否关闭了恶意软件保护?等等。

等等。

以下是disk.go使用双引导 Windows 和 Linux 的同一台机器的一些结果。

视窗:

>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

实际上,它们是相同的,disk.go持续时间为18 秒。操作系统之间只是在计算用户时间和计算内核或系统时间方面有所不同。流逝时间或实时时间是相同的。


在您的测试中,内核或系统时间分别为 93.72% runtime.cgocall和 95.49% syscall.Syscall。


查看完整回答
反对 回复 2021-12-20
  • 1 回答
  • 0 关注
  • 139 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信