猿问

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

将一个简单的程序从 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

    }


倚天杖
浏览 140回答 1
1回答

浮云间

Go 不执行文件 I/O,它将任务委托给操作系统。请参阅 Go 操作系统依赖syscall包。Linux 和 Windows 是具有不同 OS ABI 的不同操作系统。例如,Linux 使用系统调用 viasyscall.Syscall而 Windows 使用 Windows dll。在 Windows 上,dll 调用是 C 调用。它不使用cgo.&nbsp;它不会去通过使用相同的动态的C指针检查cgo,runtime.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 diskDuration : 18.3300322sElapsed time&nbsp; &nbsp;: 18.38Kernel time&nbsp; &nbsp; : 13.71 (74.6%)User time&nbsp; &nbsp; &nbsp; : 4.62 (25.1%)Linux:$ go build disk.go$ time ./diskDuration : 18.54350723sreal&nbsp; &nbsp; 0m18.547suser&nbsp; &nbsp; 0m2.336ssys&nbsp; &nbsp; &nbsp;0m16.236s实际上,它们是相同的,disk.go持续时间为18 秒。操作系统之间只是在计算用户时间和计算内核或系统时间方面有所不同。流逝时间或实时时间是相同的。在您的测试中,内核或系统时间分别为 93.72% runtime.cgocall和 95.49% syscall.Syscall。
随时随地看视频慕课网APP

相关分类

Go
我要回答