场景:
我想快速阅读大型文本文件(例如,在下面的示例中,lorem.txt
的行数为4.5毫米)。我在下面的代码中尝试了三种不同的方式。
伪基准: 这是我在下面运行此命令时得到的典型输出。下面的输出是快速而肮脏的时间增量,而不是完整的性能分析/基准测试/测试。
sequential: 43.541828091 secs 4714074 lines
queued channel: 80.986544385 secs 4714074 lines
wait group: 260.200473751 secs 4712266 lines
问题: 为什么顺序循环比下面的其他两种方法快?我想念什么吗?
更新 我在更大的文本文件上运行了示例代码(请参见场景)。我还“重置”了每个示例的记录器,以防万一不这样做,示例函数之间会出现一些复合内存问题。另外,正如其他人指出的那样,我的计算机是双核的,这可能是我的代码中的许多问题之一。感谢您的所有反馈意见/答案。
package main
import (
"bufio"
"bytes"
"fmt"
"log"
"os"
"sync"
"time"
)
var (
textFile = "lorem.txt"
buf bytes.Buffer
l = log.New(&buf, "logger: ", log.Lshortfile)
wg sync.WaitGroup
delta1 float64
delta2 float64
delta3 float64
cnt1 = 0
cnt2 = 0
cnt3 = 0
)
func main() {
// Wait Group Example
exampleWaitGroup()
// Queued Channel Example
exampleQueuedChannel()
// Sequential Loop Example
exampleSequentialLoop()
benchmarks := fmt.Sprintf("sequential:\t%v secs %v lines\nqueued channel:\t%v secs %v lines\nwait group:\t%v secs %v lines\n",
delta1, cnt1,
delta2, cnt2,
delta3, cnt3,
)
fmt.Println(benchmarks)
}
func exampleSequentialLoop() {
buf.Reset()
l = log.New(&buf, "logger: ", log.Lshortfile)
start := time.Now()
file1, err := os.Open(textFile)
if err != nil {
log.Fatal(err)
}
defer file1.Close()
scanner := bufio.NewScanner(file1)
for scanner.Scan() {
cnt1++
l.Println(scanner.Text())
}
end := time.Now()
delta1 = end.Sub(start).Seconds()
}
func exampleQueuedChannel() {
buf.Reset()
l = log.New(&buf, "logger: ", log.Lshortfile)
start := time.Now()
queue := make(chan string)
done := make(chan bool)
go processQueue(queue, done)
file2, err := os.Open(textFile)
if err != nil {
log.Fatal(err)
}
defer file2.Close()
scanner := bufio.NewScanner(file2)
for scanner.Scan() {
queue <- scanner.Text()
}
end := time.Now()
delta2 = end.Sub(start).Seconds()
}
func exampleWaitGroup() {
buf.Reset()
l = log.New(&buf, "logger: ", log.Lshortfile)
start := time.Now()
file3, err := os.Open(textFile)
if err != nil {
log.Fatal(err)
}
defer file3.Close()
scanner := bufio.NewScanner(file3)
for scanner.Scan() {
wg.Add(1)
go func(line string) {
defer wg.Done()
l.Println(line)
cnt3++
}(scanner.Text())
}
wg.Wait()
end := time.Now()
delta3 = end.Sub(start).Seconds()
}
func processQueue(queue chan string, done chan bool) {
for line := range queue {
l.Println(line)
cnt2++
}
done <- true
}
答案 0 :(得分:5)
在exampleQueuedChannel
中,您没有并行执行任何操作。是的,您已经启动了另一个goroutine,但是没有并行处理。原因是queue
是一个无缓冲通道。当您写入无缓冲的chan时,写入器将阻塞,直到有人将其读出为止。因此,基本上,您在编写时会阻塞,然后调度程序必须使goroutine进入睡眠状态,然后唤醒读取goroutine。然后那个人睡着了,作家又醒了。因此,您在两个goroutine之间苦苦挣扎,而调度程序正进行繁重的锻炼。
如果要在此处获得更好的性能,请使用缓冲通道。而且,如果您想获得更高的性能,请在每条chan消息 中添加多个项目(有关通道的性能影响的详细技术说明,请阅读this) 。
在exampleWaitGroup
中,您将为每行启动一个新的goroutine。虽然启动新的goroutine并不昂贵,但它也不是免费的,并且对于调度程序来说还需要更多工作。 defer
is also not free。也是您的记录器uses a mutex,因此,如果您的两个goroutine尝试同时登录,则其中一个将进入睡眠状态,并且还会有更多的调度程序工作。
您可以通过launching your code under a profiler自行调查这些问题,并调查瓶颈所在。
答案 1 :(得分:2)
在回答问题之前,我想指出您的基准测试方法有问题。我不会说200行的文件太大或足够用于基准测试。 Go中存在基准测试的“官方”方式,您可以在testing的文档中阅读。
有一个Go成语,可能是最著名的说:并发不是并行性。人们最期望使程序运行更快的是并行,而不是并发。实际上,在不可能实现并行的单核CPU上,并发性通常会使事情变慢,因为在goroutine(线程,协程等)之间进行切换会产生成本
在您的代码中,这很像单核的情况。并行不多,但在goroutine之间却有很多切换。此外,fmt.Println
包含IO操作,并且该操作需要同步,这无法从并行处理中受益。
答案 2 :(得分:1)
我认为不应该提高性能。在生产者/消费者方案中,仅当您的消费者比生产者慢时才引入并发(对于消费者)才有意义。您应该期望通过引入多个使用者来获得那种性能提升。
但是这里的消费者已经比生产者(IO)快得多,因此没有性能提升。