【问题标题】:Why is this Go code the equivalent speed as that of Python (and not much faster)?为什么这个 Go 代码的速度与 Python 的速度相当(而且没有快多少)?
【发布时间】:2015-01-15 14:19:01
【问题描述】:

我需要为超过 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/内核是否有助于改进?我期望至少有 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

使用no频道的版本:

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)

【问题讨论】:

  • 无论如何,我不希望它会[多]快。大部分工作是在 IO 层或哈希层(由其他人高效编写,用 C 语言为 Python)完成的:这并不是真正的“Python 代码”与“Go 代码”。
  • 你能解释一下测试结果吗?数字/时间是什么意思?
  • output of the time命令,基本上我用time ./checksum或者time python checksum.py
  • 您是否尝试过使用并非全为零的文件顺便说一句?
  • 另外,我已经用其他几个哈希函数运行了代码,sha512 快两倍,md5 x5 快,sha1 快 x3。

标签: performance go checksum


【解决方案1】:

您的解决方案效率很低,因为您在每次迭代中都制作新的缓冲区,您使用它们一次然后就扔掉它们。

您还将缓冲区的内容 (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() 是一个函数,它将从Reader 读取所有数据(直到达到 EOF)并将所有这些数据写入指定的Writer。由于 sha256 计算器 (hash.Hash) 实现了Writer,而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 个 goroutine 的完整、可运行的解决方案,其中 1 个 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 个 goroutine 并行运行。每个 goroutine 都可以随时使用其本地缓冲区b。对共享File和共享Hash的访问由通道同步,在任何给定时间只允许1个goroutine使用Hash,并且只允许从@使用(读取)1个goroutine 987654347@ 在任何给定时间。

【讨论】:

  • 如果我是对的,这会将所有文件读入内存,如何通过分块读取更有效地做到这一点?
  • @nbari No. io.Copy() 不会将整个文件读入内存,它使用“小”内部缓冲区从源读取块并将其写入目标。此内部缓冲区大小为 32KB (io.Copy() source code)。
  • @nbari 您能否发布您的性能结果,将这种方法与 python 解决方案进行比较?
  • 更新了我的答案以包含您的性能测试结果并考虑并行执行。
  • 您能告诉我如何仅使用通道进行测试吗?
【解决方案2】:

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

https://blog.golang.org/pipelines

在本页的最后,有一个 goroutines 对 md5 文件的解决方案。

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

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

【讨论】:

    猜你喜欢
    • 2020-05-14
    • 1970-01-01
    • 1970-01-01
    • 2023-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-02
    相关资源
    最近更新 更多