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这种用法。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。