Почему последовательные циклы работают быстрее, чем параллельные методы в Go? - PullRequest
0 голосов
/ 24 июня 2018

Сценарий: Я хочу быстро прочитать большой текстовый файл (например, lorem.txt с 4,5 миллионными строками в примере ниже).Я попробовал три различных способа в приведенном ниже коде.

  1. Одновременная последовательная обработка
  2. Каналы и goroutines
  3. Waitgroup и goroutines

Псевдо-тесты: Вот типичный вывод, который я получаю, когда запускаю это ниже;ниже приводятся быстрые и грязные дельты времени, а не полноценное профилирование / стендовая разметка / тестирование.

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
}

Ответы [ 3 ]

0 голосов
/ 24 июня 2018

Прежде чем ответить на вопрос, я хотел бы отметить, что ваш метод сравнительного анализа проблематичен.Я бы не сказал, что файл из 200 строк достаточно велик или достаточен для сравнительного анализа.И есть «официальный» способ сравнительного анализа в Go, который вы можете прочитать в документах testing .

Существует идиома Go, вероятно, самая известная, говорящая: Параллелизм не есть параллелизм .То, что больше всего заставит программу работать быстрее, это параллелизм , а не параллелизм .Фактически, на одноядерном процессоре, где параллелизм невозможен, параллелизм обычно замедляет работу, потому что переключение между процедурами (потоками, сопрограммами и т. Д.) Обходится дорого.

В вашем коде это очень похоже на ситуацию с одноядерным.Существует не так много пареллизма, но много переключений между программами.Кроме того, fmt.Println включает в себя операцию ввода-вывода, и эта операция требует синхронизации, что не может быть выгодно от пареллизма.

0 голосов
/ 24 июня 2018

Я не думаю, что должно быть повышение производительности. В сценарии производитель / потребитель введение параллелизма (для потребителей) имеет смысл, только если ваш потребитель медленнее, чем производитель. В этом случае следует ожидать увеличения производительности, представив несколько потребителей.

Но здесь потребитель уже значительно быстрее, чем производитель (IO), следовательно, прирост производительности отсутствует.

0 голосов
/ 24 июня 2018

В exampleQueuedChannel вы ничего не делаете параллельно.Да, вы запустили другую программу, но параллельной обработки нет.Причина в том, что queue - небуферизованный канал.Когда вы пишете в небуферизованный канал, писатель блокируется, пока кто-то не прочитает его.Таким образом, по существу, вы блокируете во время записи, а затем планировщик должен уложить goroutine в режим сна и разбудить goroutine для чтения.Затем этот человек ложится спать, и писатель снова просыпается.Таким образом, вы перебираете между двумя программами, а планировщик получает тяжелую тренировку.
Если вы хотите получить более высокую производительность здесь, используйте буферизованный канал.А если вам нужна еще более высокая производительность, суммируйте несколько элементов в одном сообщении канала (для подробного технического объяснения влияния каналов на производительность прочитайте this ) .

В exampleWaitGroup вы запускаете новую программу для каждой строки.Хотя запуск новых подпрограмм стоит недорого, он также не бесплатен, а также требует больше времени для планировщика.defer также не бесплатно .Кроме того, ваш регистратор использует мьютекс , поэтому, если две из ваших программ попытаются войти в систему одновременно, одна из них будет переведена в спящий режим, и снова будет работать больше планировщика.

Выможете самостоятельно разобраться в этих вопросах, запустив код под профилировщиком и выяснив, где находятся узкие места.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...