Go语言中三种不同md5计算方式的性能比较
前言
本文主要介绍的是三种不同的md5计算方式,其实区别是读文件的不同,也就是磁盘I/O,所以也可以举一反三用在网络I/O上。下面来一起看看吧。
ReadFile
先看第一种,简单粗暴:
funcmd5sum1(filestring)string{
data,err:=ioutil.ReadFile(file)
iferr!=nil{
return""
}
returnfmt.Sprintf("%x",md5.Sum(data))
}
之所以说其粗暴,是因为ReadFile里面其实调用了一个readall,分配内存是最多的。
Benchmark来一发:
vartest_path="/path/to/file"
funcBenchmarkMd5Sum1(b*testing.B){
fori:=0;i<b.N;i++{
md5sum1(test_path)
}
}
gotest-test.run=none-test.bench="^BenchmarkMd5Sum1$"-benchtime=10s-benchmem BenchmarkMd5Sum1-430043704982ns/op19408224B/op14allocs/op PASS oktmp17.446s
先说明下,这个文件大小是19405028字节,和上面的19408224B/op非常接近,因为readall确实是分配了文件大小的内存,代码为证:
ReadFile源码
//ReadFilereadsthefilenamedbyfilenameandreturnsthecontents.
//Asuccessfulcallreturnserr==nil,noterr==EOF.BecauseReadFile
//readsthewholefile,itdoesnottreatanEOFfromReadasanerror
//tobereported.
funcReadFile(filenamestring)([]byte,error){
f,err:=os.Open(filename)
iferr!=nil{
returnnil,err
}
deferf.Close()
//It'sagoodbutnotcertainbetthatFileInfowilltellusexactlyhowmuchto
//read,solet'stryitbutbepreparedfortheanswertobewrong.
varnint64
iffi,err:=f.Stat();err==nil{
//Don'tpreallocateahugebuffer,justincase.
ifsize:=fi.Size();size<1e9{
n=size
}
}
//AsinitialcapacityforreadAll,usen+alittleextraincaseSizeiszero,
//andtoavoidanotherallocationafterReadhasfilledthebuffer.ThereadAll
//callwillreadintoitsallocatedinternalbuffercheaply.Ifthesizewas
//wrong,we'lleitherwastesomespaceofftheendorreallocateasneeded,but
//intheoverwhelminglycommoncasewe'llgetitjustright.
//readAll第二个参数是即将创建的buffer大小
returnreadAll(f,n+bytes.MinRead)
}
funcreadAll(rio.Reader,capacityint64)(b[]byte,errerror){
//这个buffer的大小就是filesize+bytes.MinRead
buf:=bytes.NewBuffer(make([]byte,0,capacity))
//Ifthebufferoverflows,wewillgetbytes.ErrTooLarge.
//Returnthatasanerror.Anyotherpanicremains.
deferfunc(){
e:=recover()
ife==nil{
return
}
ifpanicErr,ok:=e.(error);ok&&panicErr==bytes.ErrTooLarge{
err=panicErr
}else{
panic(e)
}
}()
_,err=buf.ReadFrom(r)
returnbuf.Bytes(),err
}
io.Copy
再看第二种,
funcmd5sum2(filestring)string{
f,err:=os.Open(file)
iferr!=nil{
return""
}
deferf.Close()
h:=md5.New()
_,err=io.Copy(h,f)
iferr!=nil{
return""
}
returnfmt.Sprintf("%x",h.Sum(nil))
}
第二种的特点是:使用了io.Copy。在一般情况下(特殊情况在下面会提到),io.Copy每次会分配32*1024字节的内存,即32KB,然后咱看下Benchmark的情况:
funcBenchmarkMd5Sum2(b*testing.B){
fori:=0;i<b.N;i++{
md5sum2(test_path)
}
}
$gotest-test.run=none-test.bench="^BenchmarkMd5Sum2$"-benchtime=10s-benchmem BenchmarkMd5Sum2-450037538305ns/op33093B/op8allocs/op PASS oktmp22.657s
32*1024=32768,和上面的33093B/op很接近。
io.Copy+bufio.Reader
然后再看看第三种情况。
这次不仅用了io.Copy,还用了bufio.Reader。bufio顾名思义,即bufferedI/O,性能相对要好些。bufio.Reader默认会创建4096字节的buffer。
funcmd5sum3(filestring)string{
f,err:=os.Open(file)
iferr!=nil{
return""
}
deferf.Close()
r:=bufio.NewReader(f)
h:=md5.New()
_,err=io.Copy(h,r)
iferr!=nil{
return""
}
returnfmt.Sprintf("%x",h.Sum(nil))
}
看下Benchmark的情况:
funcBenchmarkMd5Sum3(b*testing.B){
fori:=0;i<b.N;i++{
md5sum3(test_path)
}
}
$gotest-test.run=none-test.bench="^BenchmarkMd5Sum3$"-benchtime=10s-benchmem BenchmarkMd5Sum3-430042589812ns/op4507B/op9allocs/op PASS oktmp16.817s
上面的4507B/op是不是和4096很接近?那为什么io.Copy+bufio.Reader的方式所用内存会比单纯的io.Copy占用内存要少一些呢?上文也提到,一般情况下io.Copy每次会分配32*1024字节的内存,那特殊情况是?答案在源码中。
一起看看io.Copy相关源码:
funcCopy(dstWriter,srcReader)(writtenint64,errerror){
returncopyBuffer(dst,src,nil)
}
//copyBufferistheactualimplementationofCopyandCopyBuffer.
//ifbufisnil,oneisallocated.
funccopyBuffer(dstWriter,srcReader,buf[]byte)(writtenint64,errerror){
//IfthereaderhasaWriteTomethod,useittodothecopy.
//Avoidsanallocationandacopy.
//hash.Hash这个Writer并没有实现WriteTo方法,所以不会走这里
ifwt,ok:=src.(WriterTo);ok{
returnwt.WriteTo(dst)
}
//Similarly,ifthewriterhasaReadFrommethod,useittodothecopy.
//而bufio.Reader实现了ReadFrom方法,所以,会走这里
ifrt,ok:=dst.(ReaderFrom);ok{
returnrt.ReadFrom(src)
}
ifbuf==nil{
buf=make([]byte,32*1024)
}
for{
nr,er:=src.Read(buf)
ifnr>0{
nw,ew:=dst.Write(buf[0:nr])
ifnw>0{
written+=int64(nw)
}
ifew!=nil{
err=ew
break
}
ifnr!=nw{
err=ErrShortWrite
break
}
}
ifer==EOF{
break
}
ifer!=nil{
err=er
break
}
}
returnwritten,err
}
从上面的源码来看,用bufio.Reader实现的io.Reader并不会走默认的buffer创建路径,而是提前返回了,使用了bufio.Reader创建的buffer,这也是使用了bufio.Reader分配的内存会小一些。
当然如果你希望io.Copy也分配小一点的内存,也是可以做到的,不过是用io.CopyBuffer,buf就创建一个4096的[]byte即可,就跟bufio.Reader区别不大了。
看看是不是这样:
//Md5Sum2用CopyBufer重新实现,buf:=make([]byte,4096) BenchmarkMd5Sum2-450038484425ns/op4409B/op8allocs/op BenchmarkMd5Sum3-450038671090ns/op4505B/op9allocs/op
从结果来看,分配的内存相差不大,毕竟实现不一样,不可能一致。
那下次如果你要写一个下载大文件的程序,你还会用ioutil.ReadAll(resp.Body)吗?
最后整体对比下Benchmark的情况:
$gotest-test.run=none-test.bench="."-benchtime=10s-benchmem testing:warning:noteststorun BenchmarkMd5Sum1-430042551920ns/op19408230B/op14allocs/op BenchmarkMd5Sum2-450038445352ns/op33089B/op8allocs/op BenchmarkMd5Sum3-450038809429ns/op4505B/op9allocs/op PASS oktmp63.821s
小结
这三种不同的md5计算方式在执行时间上都差不多,区别最大的是内存的分配上;
bufio在处理I/O还是很有优势的,优先选择;
尽量避免ReadAll这种用法。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。