将一个简单的程序从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
答案 0 :(得分:6)
Go不执行文件I / O,它将任务委派给操作系统。请参阅Go操作系统相关的syscall
包。
Linux和Windows是具有不同OS ABI的不同操作系统。例如,Linux通过syscall.Syscall
使用系统调用,Windows使用Windows dll。在Windows上,dll调用是C调用。它没有使用cgo
。它确实经过cgo
,runtime.cgocall
使用的相同动态C指针检查。没有runtime.wincall
别名。
总之,不同的操作系统具有不同的OS调用机制。
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
。