计算Go中切片中字符的出现次数

好的,所以我碰到了一堵砖墙。

编辑:在我的count()函数中使用bytes.IndexByte()使其运行速度几乎快两倍。 bytes.IndexByte()是用汇编而不是Go编写的。 仍然不是C速度,但更接近。

我有两个程序,一个在C中,一个在Go中,它们都计算文件中的换行符。 超级简单。 C程序运行~1.5秒,在2.4GB文件中运行~4.25秒。

我是否达到了Go的限速? 如果是这样, 究竟什么造成了这种情况呢? 我可以阅读C,但我无法读取汇编,所以比较C的asm和Go的asm对我没什么用,除了表明Go还有400多行(忽略.ascii部分)。

虽然我知道Go无法匹配C步骤,但我不会假设4x减速。

想法?

这是Go的cpuprofile: 在此处输入图像描述

这是C(编译w / gcc -Wall -pedantic -O9

 #include  #include  #include  #include  #include  #include  #include  #include  #define BUFFER_SIZE (16 * 1024) int main() { const char *file = "big.txt"; int fd = open (file, O_RDONLY); char buf[BUFFER_SIZE + 1]; uintmax_t bytes; size_t bytes_read; size_t lines; posix_fadvise (fd, 0, 0, POSIX_FADV_SEQUENTIAL); while ((bytes_read = safe_read (fd, buf, BUFFER_SIZE)) > 0) { char *p = buf; // error checking while ((p = memchr (p, '\n', (buf + bytes_read) - p))) { ++p; ++lines; } bytes += bytes_read; } printf("%zu\n", bytes); printf("%zu\n", lines); return 0; } 

Go:

 package main import ( "flag" "fmt" "io" "os" "runtime/pprof" "syscall" ) const ( POSIX_FADV_SEQUENTIAL = 2 NewLineByte = '\n' // or 10 BufferSize = (16 * 1024) + 1 ) var Buffer = make([]byte, BufferSize) func fadvise(file *os.File, off, length int, advice uint32) error { _, _, errno := syscall.Syscall6(syscall.SYS_FADVISE64, file.Fd(), uintptr(off), uintptr(length), uintptr(advice), 0, 0) if errno != 0 { return errno } return nil } func count(s []byte) int64 { count := int64(0) for i := 0; i < len(s); i++ { if s[i] == NewLineByte { count++ } } return count } func main() { file, err := os.Open("big.txt") if err != nil { panic(err) } var lines int64 var bytes int64 fadvise(file, 0, 0, POSIX_FADV_SEQUENTIAL) for { n, err := file.Read(Buffer) if err != nil && err != io.EOF { panic(err) } lines += count(Buffer[:n]) bytes += int64(n) if err == io.EOF { break } } fmt.Printf("%d\n", bytes) fmt.Printf("%d\n", lines) } 

至于这是关于计算文件中的'\n' ,这个代码运行在~1.26秒(并且大多数更快),在Zorin VM(VMWare播放器),6 GB RAM,4个核心(和电源插入;因为电源管理器有时会阻止CPU过快耗电;主机操作系统是Windows 8.我在一些现实世界的项目中使用Go不到6个月,我也是Linux菜鸟。 但我认为问题是从Go调用C并且这比纯Go慢得多 – 我在调用一些C代码时遇到过这种情况,无论是作为dll还是用cgo编译的。

 package main import ( "fmt" "io" "os" "runtime" "time" ) func main() { tstart := time.Now() file, err := os.Open(filePath) if err != nil { panic(err) } defer file.Close() done := make(chan bool) var cnt int64 = 0 go func() { var Buffer = make([]byte, BufferSize) for { n, err := file.Read(Buffer) if err != nil && err != io.EOF { panic(err) } cnt += count(Buffer[:n]) if err == io.EOF { done <- true return } } }() <-done // should be 5860298 in this case (zorin iso image) & it is. fmt.Println(cnt) fmt.Printf("%s took %s\n", "counting", time.Since(tstart)) } func count(s []byte) int64 { count := int64(0) for i := 0; i < len(s); i++ { if s[i] == NewLineByte { count++ } } return count } const ( NewLineByte = '\n' // or 10 BufferSize = 32 * 1024 ) var ( filePath = "/.../zorin-os-9.1-core-64++.iso" maxt int ) func init() { maxt = runtime.NumCPU() runtime.GOMAXPROCS(maxt) } 

这是一个不太难但不太慢的方法,使用bytes.IndexByte (因为你发现Go的asm实现有帮助)和syscall.Mmap

 package main import ( "bytes" "fmt" "log" "os" "syscall" ) func main() { if len(os.Args) < 2 { log.Fatal("pass filename on command line") } f, err := os.Open(os.Args[1]) if err != nil { log.Fatal("open: ", err) } stat, err := f.Stat() if err != nil { log.Fatal("stat: ", err) } data, err := syscall.Mmap(int(f.Fd()), 0, int(stat.Size()), syscall.PROT_READ, syscall.MAP_SHARED) if err != nil { log.Fatal("mmap: ", err) } newlines := 0 for { i := bytes.IndexByte(data, 10) if i == -1 { break } newlines++ data = data[i+1:] } fmt.Println(newlines) } 

Mmap看起来很奇怪,但在这里,就好像你将文件读入切片一样,除了操作系统的帮助,资源密集程度更低。

您可以在没有太多工作的情况下并行计数 ,但我不确定这是值得的。 (如果amd64的增益为零或为负,例如单核计数受内存带宽的限制,那我就不会感到震惊,但这对我来说测试速度不快。)