Уменьшение нагрузки на ядро ​​при чтении огромного файла с ленивыми строками - PullRequest
6 голосов
/ 31 января 2020

Я читаю большой файл (1-10 ГБ) и вычисляю некоторую простую статистику c, например, подсчет символа. Потоковая передача имеет смысл в этом контексте, поэтому я использую lazy ByteString s. В частности, мой main выглядит как

import qualified Data.ByteString.Lazy as BSL

main :: IO ()
main = do
  contents <- BSL.readFile "path"
  print $ computeStats contents

Подробности computeStats, вероятно, не важны в этом контексте.

Когда я запускаю это с +RTS -sstderr, я вижу это :

MUT     time    0.938s  (  1.303s elapsed)

Обратите внимание на разницу между временем процессора и прошедшим временем. Кроме того, выполнение под /usr/bin/time показывает аналогичные результаты:

0.89user 0.45system 0:01.35elapsed

Файл, с которым я тестирую, находится в tmpfs, поэтому фактическая производительность диска не должна быть фактором.

Как я могу уменьшить system время в этом случае? Я попытался явно установить размер буфера для дескриптора файла (без статистически значимого влияния на время выполнения), а также mmap файл и обернуть его в ByteString (время выполнения фактически ухудшилось). Что еще стоит попробовать?

1 Ответ

2 голосов
/ 01 февраля 2020

Во-первых, кажется, что с вашей машиной происходит что-то нехорошее. Когда я запускаю эту программу для файла 1G, кэшированного в памяти или в файловой системе tmpfs (неважно, какая), системное время существенно меньше:

1.44user 0.14system 0:01.60elapsed 99%CPU (0avgtext+0avgdata 50256maxresident)

Если у вас есть какая-либо другая загрузка или Нагрузка памяти, которая может составлять эти дополнительные 300 мсек, думаю, вам нужно сначала решить эту проблему, прежде чем что-то, что я скажу ниже, поможет, но ...

В любом случае, для своих тестов я использовал больший 5G testfile для упрощения количественного определения системного времени. Исходя из этого, программа C:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define BUFLEN (1024*1024)
char buffer[BUFLEN];

int
main()
{
        int nulls = 0;
        int fd = open("/dev/shm/testfile5G.dat", O_RDONLY);
        while (read(fd, buffer, BUFLEN) > 0) {
                for (int i = 0; i < BUFLEN; ++i) {
                        if (!buffer[i]) ++nulls;
                }
        }
        printf("%d\n", nulls);
}

, скомпилированная с gcc -O2, запускается в моем тестовом файле со временем:

real    0m2.035s
user    0m1.619s
sys     0m0.416s

Для сравнения, программа Haskell, скомпилированная с ghc -O2:

import Data.Word
import qualified Data.ByteString.Lazy as BSL

main :: IO ()
main = do
  contents <- BSL.readFile "/scratch/buhr/testfile5G.dat"
  print $ BSL.foldl' go 0 contents
    where go :: Int -> Word8 -> Int
          go n 0 = n + 1
          go n _ = n

немного медленнее в подсчете, но имеет почти эквивалентное системное время:

real    0m8.411s
user    0m7.966s
sys     0m0.444s

Все более простые тесты, такие как cat testfile5G.dat >/dev/null, дают согласованные результаты по системному времени, поэтому можно с уверенностью заключить, что накладные расходы на read вызовы, скорее всего, специфический c процесс копирования данных из ядра в адресное пространство пользователя, составляют значительную долю в 410 мсек или около того системного времени.

Вопреки вашему опыту выше, переключение на mmap должно уменьшить эти издержки. Программа Haskell:

import System.Posix.IO
import Foreign.Ptr
import Foreign.ForeignPtr
import MMAP
import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BS

-- exact length of file
len :: Integral a => a
len = 5368709120

main :: IO ()
main = do
  fd <- openFd "/scratch/buhr/testfile5G.dat" ReadOnly Nothing defaultFileFlags
  ptr <- newForeignPtr_ =<< castPtr <$>
    mmap nullPtr len protRead (mkMmapFlags mapPrivate mempty) fd 0
  let contents = BS.fromForeignPtr ptr 0 len
  print $ BS.foldl' (+) 0 contents

работает примерно с тем же временем пользователя, но существенно сокращает системное время:

real    0m7.972s
user    0m7.791s
sys     0m0.181s

Обратите внимание, что абсолютно необходимо использовать метод нулевой копии превращая отображаемую область в строгую ByteString здесь.

На данный момент, я думаю, мы, вероятно, дошли до издержек на управление таблицами страниц процесса и на версию C с использованием mmap:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>

size_t len = 5368709120;

int
main()
{
        int nulls = 0;
        int fd = open("/scratch/buhr/testfile5G.dat", O_RDONLY);
        char *p = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0);
        for (int i = 0; i < len; ++i) {
                if (!p[i]) ++nulls;
        }
        printf("%d\n", nulls);
}

имеет похожее системное время:

real    0m1.888s
user    0m1.708s
sys     0m0.180s
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...