为什么这段 Go 代码的速度与 Python 的速度相当(而且快不了多少)?

Why is this Go code the equivalent speed as that of Python (and not much faster)?

我需要为超过 1GB 的文件计算 sha256 校验和(按块读取文件),目前我正在使用 python:

import hashlib
import time

start_time = time.time()


def sha256sum(filename="big.txt", block_size=2 ** 13):
    sha = hashlib.sha256()
    with open(filename, 'rb') as f:
        for chunk in iter(lambda: f.read(block_size), b''):
           sha.update(chunk)
    return sha.hexdigest()

input_file = '/tmp/1GB.raw'
print 'checksum is: %s\n' % sha256sum(input_file)
print 'Elapsed time: %s' % str(time.time() - start_time)

我想尝试 golang 认为我可以获得更快的结果,但在尝试以下代码后,它运行速度慢了几秒钟:

package main

import (
    "crypto/sha256"
    "fmt"
    "io"
    "math"
    "os"
    "time"
)   

const fileChunk = 8192

func File(file string) string {
    fh, err := os.Open(file)

    if err != nil {
        panic(err.Error())
    }   

    defer fh.Close()

    stat, _ := fh.Stat()
    size := stat.Size()
    chunks := uint64(math.Ceil(float64(size) / float64(fileChunk)))
    h := sha256.New()

    for i := uint64(0); i < chunks; i++ {
        csize := int(math.Min(fileChunk, float64(size-int64(i*fileChunk))))
        buf := make([]byte, csize)
        fh.Read(buf)
        io.WriteString(h, string(buf))
    }   

    return fmt.Sprintf("%x", h.Sum(nil))
}   

func main() {
    start := time.Now()
    fmt.Printf("checksum is: %s\n", File("/tmp/1G.raw"))
    elapsed := time.Since(start)
    fmt.Printf("Elapsed time: %s\n", elapsed)
}

知道如何改进 golang 代码吗?也许使用所有计算机 CPU 核心,一个用于阅读,另一个用于散列,有什么想法吗?

更新

按照建议,我正在使用此代码:

package main

import (
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "io"
    "os"
    "time"
)

func main() {
    start := time.Now()
    fh, err := os.Open("/tmp/1GB.raw")
    if err != nil {
        panic(err.Error())
    }
    defer fh.Close()

    h := sha256.New()
    _, err = io.Copy(h, fh)
    if err != nil {
        panic(err.Error())
    }
    fmt.Println(hex.EncodeToString(h.Sum(nil)))

    fmt.Printf("Elapsed time: %s\n", time.Since(start))
}

为了测试,我用这个创建了 1GB 的文件:

# mkfile 1G /tmp/1GB.raw

新版本速度更快但没那么快,使用渠道怎么样?使用多个 CPU/core 可以帮助改进吗?我期望至少有 20% 的改进,但不幸的是我几乎没有任何收获,几乎没有。

python

的时间结果
 5.867u 0.250s 0:06.15 99.3%    0+0k 0+0io 0pf+0w

编译(go build)和执行二进制文件后 go 的时间结果:

 5.687u 0.198s 0:05.93 98.9%    0+0k 0+0io 0pf+0w

还有什么想法吗?

测试结果

使用在@icza

接受的答案中发布的渠道版本
Elapsed time: 5.894779733s

使用频道的版本:

Elapsed time: 5.823489239s

我以为使用频道会增加一点,但似乎不会。

我在 MacBook Pro OS X Yosemite 上 运行。使用 go 版本:

go version go1.4.1 darwin/amd64

更新 2

将 runtime.GOMAXPROCS 设置为 4:

runtime.GOMAXPROCS(4)

让事情变得更快:

Elapsed time: 5.741511748s

更新 3

将块大小更改为 8192(与 python 版本中的一样)给出预期结果:

...
for b, hasMore := make([]byte, 8192<<10), true; hasMore; {
...

也仅使用 runtime.GOMAXPROCS(2)

您的解决方案效率很低,因为您在每次迭代中都创建了新的缓冲区,您只使用了一次就把它们扔掉了。

您还将缓冲区的内容 (buf) 转换为 string,然后将 string 写入 sha256 计算器,后者将其转换回字节:绝对不必要的回合-旅行。

这是另一个非常快速的解决方案,测试一下它的性能:

fh, err := os.Open(file)
if err != nil {
    panic(err.Error())
}   
defer fh.Close()

h := sha256.New()
_, err = io.Copy(h, fh)
if err != nil {
    panic(err.Error())
}   

fmt.Println(hex.EncodeToString(h.Sum(nil)))

一点解释:

io.Copy() is a function which will read all the data (until EOF is reached) from a Reader and write all those to the specified Writer. Since the sha256 calculator (hash.Hash) implements Writer and the File(或者更确切地说 *File)实现了 Reader,这非常简单。

一旦所有数据都写入哈希,hex.EncodeToString() 将简单地将结果(由 hash.Sum(nil) 获得)转换为人类可读的十六进制字符串。

最终判决

该程序从硬盘读取 1GB 数据并对其进行一些计算(计算其 SHA-256 哈希值)。由于从硬盘读取是一个相对较慢的操作,Go 版本的性能提升与 Python 方案相比并不显着。总体 运行 需要几秒钟,这与从硬盘读取 1 GB 数据所需的时间处于同一数量级。由于 Go 和 Python 解决方案从磁盘读取数据所需的时间大致相同,因此您不会看到太多不同的结果。

使用多个 Goroutine 提高性能的可能性

您可以通过将文件的一个块读入一个缓冲区,开始计算其 SHA-256 哈希,同时读取文件的下一个块来提高性能。完成后,将其发送到 SHA-256 计算器,同时将下一个块读入第一个缓冲区。

但是由于从磁盘读取数据比计算其 SHA-256 摘要(或更新摘要计算器的状态)花费更多的时间,因此您不会看到明显的改进。您的性能瓶颈始终是将数据读入内存所需的时间。

这是一个完整的、运行可用的解决方案,它使用 2 个 goroutines,其中一个 goroutine 读取文件的一个块,另一个 goroutine 计算先前读取的块的哈希值,当 goroutine 的读取完成时继续散列并允许另一个并行读取。

阶段(读取、散列)之间的正确同步是通过通道完成的。正如所怀疑的那样,性能增益在时间 4% 上略多一点(可能因 CPU 和硬盘速度而异)因为哈希计算与磁盘相比可以忽略不计阅读时间。如果硬盘的读取速度更快,性能增益可能会更高(在SSD上测试)。

所以完整的程序:

package main

import (
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "hash"
    "io"
    "os"
    "runtime"
    "time"
)

const file = "t:/1GB.raw"

func main() {
    runtime.GOMAXPROCS(2) // Important as Go 1.4 uses only 1 by default!

    start := time.Now()

    f, err := os.Open(file)
    if err != nil {
        panic(err)
    }
    defer f.Close()

    h := sha256.New()

    // 2 channels: used to give green light for reading into buffer b1 or b2
    readch1, readch2 := make(chan int, 1), make(chan int, 1)

    // 2 channels: used to give green light for hashing the content of b1 or b2
    hashch1, hashch2 := make(chan int, 1), make(chan int, 1)

    // Start signal: Allow b1 to be read and hashed
    readch1 <- 1
    hashch1 <- 1

    go hashHelper(f, h, readch1, readch2, hashch1, hashch2)

    hashHelper(f, h, readch2, readch1, hashch2, hashch1)

    fmt.Println(hex.EncodeToString(h.Sum(nil)))

    fmt.Printf("Elapsed time: %s\n", time.Since(start))
}

func hashHelper(f *os.File, h hash.Hash, mayRead <-chan int, readDone chan<- int, mayHash <-chan int, hashDone chan<- int) {
    for b, hasMore := make([]byte, 64<<10), true; hasMore; {
        <-mayRead
        n, err := f.Read(b)
        if err != nil {
            if err == io.EOF {
                hasMore = false
            } else {
                panic(err)
            }
        }
        readDone <- 1

        <-mayHash
        _, err = h.Write(b[:n])
        if err != nil {
            panic(err)
        }
        hashDone <- 1
    }
}

备注:

在我的解决方案中,我只使用了 2 个 goroutine。使用更多是没有意义的,因为如前所述,磁盘读取速度是瓶颈,它已经被最大程度地使用,因为 2 个 goroutine 将能够随时执行读取。

关于同步的注意事项: 2 个 goroutines 运行 并行。每个 goroutine 都可以随时使用其本地缓冲区 b。共享 File 和共享 Hash 的访问由通道同步,在任何给定时间只允许 1 个 goroutine 使用 Hash,并且只允许 1 个 goroutine 使用在任何给定时间从 File(阅读)。

对于不知道的人,我认为这会有所帮助。

https://blog.golang.org/pipelines

本页末尾有goroutines对md5文件的解决方法

我在自己的“~”目录中尝试了这个。使用 goroutines 花费 1.7 秒,没有 goroutines 使用 2.8 秒。

这里是没有 goroutines 时的时间使用情况。而且我不知道在使用 goroutines 时如何计算时间使用,因为所有这些东西都是同时运行的。 time use 2.805522165s time read file 759.476091ms time md5 1.710393575s time sort 17.355134ms