Comments 42
Это же сравнения на релизной сборке? Ну которые в 180 раз быстрее? Было бы неплохо Бобу пробежаться с профайлером и понять, откуда такая чудовищная разница, для одних только аллокаций что-то многовато.
Скажем так - стиль требует полировки, если мы говорим о С++. Хаотичная и не идиоматичная обработка ошибок, отказ от raii, приведения типов в стиле Си, какие-то избыточные алиасы, пропавшие конструкторы и много такой фигни.
Если говорить о дизайне, по существу буфер и ридер надо разделять, иначе нулевая гибкость.
А так отличное упражнение для Боба, удачи ему.
С профайлером не стал заморачиваться, но попробовал свои бенчмарки этих двух реализаций сделать. Получилась разница в 10-20% (причём как в релизной, так и в дебажной сборках), а не в 180 раз как у автора.
Заглянул в репозитории в файлик TextFileReader_profile.cpp, который, видимо, дал такие результаты. Там функция tastyReadline (проверяющая реализацию автора) во-первых, читает неинициализированную переменную, во-вторых, при подсчёте средней длины строки пропускает деление на количество строк. В-третьих (уже касательно обеих реализаций), если эти функции заинлайнятся, то не вижу ничего, что мешало бы компилятору вообще весь этот подсчёт символов в строках нафиг соптимизировать.
IMHO, Слишком много времени уходит на решение простой задачи. Такими оптимизациями надо заниматься, если вы точно знаете, что из-за медленного считывания файла тормозит приложение. В противном случае это бисер на шее свиньи, этот код могут выкинуть ввиду каких-нибудь изменений и вся это отлаженная красота окажется бесполезной. Справедливости ради, в статье не указаны конкретные условия, но подозреваю, что когда "Бобу поручили построчно обработать текстовый файл", поручители ожидали быстрого решения, с учётом того, что Боб - джун. В таком случае, Боб зазря потратил много времени, которое могло бы уйти на разработку новых фич. В качестве упражнения, конечно, неплохо, но лучше бы товарищу научиться грамотно распределять свои ресурсы между задачами.
Но такое решение уже будет непереносимым, так как полагается на наличие у ОС такой возможности. А её может и не быть -- скажем, если программе придётся работать на микроконтроллере, где виртуальной памяти в принципе не существует.
Угу. Поэтому предлагать подобное решение можно лишь в случае, если оговорена необязательная переносимость и прочие возможные ограничения или их отсутствие. У меня, исходя из начала этой статьи, сложилось впечатление, что неявно сформулированная задача заключалась в вводе строк исключительно стандартными средствами библиотеки языка.
На микроконтроллере, скажем, и файловой системы может не быть. А на мейнфрейме, как Вам хорошо известно, файл может не быть потоком. Сама постановка задачи уже не вполне переносима.
И свои метрики производительности автор тоже получает во вполне конкретном окружении, они, в принципе, могут и не иметь универсального характера.
Насчёт ммапинга - таки лучше снять розовые очки. То, что в юзерспейсе оно выглядит очень простым - совсем не значит, что оно такое же "под капотом". Отдельно выделенный поток ядра будет "там" считывать файл кусками, класть его в кэш и создавать иллюзию целостной бесшовной работы. Разные madvise может и помогут - но всё равно прямого доступа нет, есть опосредованный через кэш. Т.е. данные ВСË РАВНО аллоцируются, пусть и не в рамках процесса, а в ядре.
Статья по запросу "Are You Sure You Want to Use MMAP in Your Database Management System" ещё больше развеет веру в маппинг.
Вкратце - он не про быстродействие, а про простоту кода в юзерспейсе.
mlock дает 'быстродействие' не только в том смысле, что одна и та же задача, используя fread будет медленнее чем mlock на десяток другой процентов (тупо потому что меньше уровней кода между пользовательским кодом и собственно данными) но и с точки зрения юзабилити.
Данные не нужно копировать! они будут скопированы однократно с диска в память и собственно все, они останутся в оперативной памяти (кеше файловой системы) даже если приложение будет закрыто и запущенно снова. И это нереально полезно и удобно.
А вот с fread проблема в том что данные все так же будут в кеше и после этого их нужно будет скопировать в пользовательское пространство, что фактически удваивает требования к оперативной памяти, если желаешь чтобы данные эффективно были размещены в кеше.
Hidden text
Пример с приложением, которое активно использует память - llama.cpp, с отключенным mlock вся языковая модель копируется в память, когда как с использованием маппинга памяти повторный запуск не тратит времени на загрузку модели, мало того, если часть модели будет вытеснена из кеша каким-либо приложением, с диска будет загружена только эта отсутствующая часть. Да, без дополнительных телодвижений не получится использовать эффективно приложение если памяти не хватает 'чуть чуть' (например модель требует 64gb оперативки но ее всего 64, а порядка еще гигабайт требует операционная система) просто потому что кеш так работает, он будет вытеснять старые данные из кеша в первую очередь вынуждая к перезагрузке всей модели а не только той маленькой части которая не влезла, но это решаемо достаточно простым кодом.
Наверное, вы всё же имели в виду mmap а не mlock.
Последний уж слишком суров, и привилегий требует, и OOM-киллер может позвать. Ну и просто не сработает, если реальной оперативки маловато. (ммапу по дефолту достаточно 5 страничек на линуксах, т.е. 20кб)
Вот обратная сторона медали - https://habr.com/ru/articles/820591/
Что такое "аппетитное решение", можно растолковать? Оно вызывает голод?
Если всё настолько плохо, что хочется избежать копирования, то mmap()
должно спасти отца русской демократии.
полностью согласен, но mmap есть только на x86 (unix/linux и windows) и arm, а к примеру если у тебя микроконтроллер, работу с файлами придется вести 'по старинке', потому что возможность отображения в адресное пространство чего то сгенерированного кодом (типа файла с диска) это аппаратная фича.
Да, для микроконтроллеров mmap не нужен.
Но если говорить именно про задачу - записывать в текстовый файл на карту памяти по строчке на событие, только изменившиеся данные.
Для этого нужно прочитать конец файла, найти конец строки, извлечь данные и сравнить их с текущими показателями.
Если пишем на любой блоковый носитель, то последний блок придётся читать хоть тушкой, хоть чучелом, потому что он [почти] при любом раскладе не до конца заполненный, и придётся в прочитанные данные новую строку (или как минимум её начало) добавить и записать изменённый блок обратно
Конечно, вы все абсолютно правы, пример надуманный, и как все надуманные примеры его можно довести до абсурда, например памяти не хватает а данных много, не в смысле сразу много пишут, а в том что файлов, в которые нужно писать - много, хоть и редко.
что мешает удерживать в памяти этот последний блок, добивать новую строку в конец и сбрасывать блок на носитель? Зачем его считывать опять?
Например, что если пропало питание, бандура стартовала с нуля — и, соответственно, в памяти ничего нет?
Строго говоря, задача поставлена крайне расплывчато и явно имеются какие-то очень-очень специфические требования, которым библиотечные функции не удовлетворяют, и я намереваюсь эти требования из автора вытянуть...
Вообще бессмысленные действия без учёта того, что и как мы дальше собираемся делать с этими строками.
Чувак тут экономит на спичках на конструкторах (вместо чего, как верно замечено выше, лучше бы использовал mmap), а потом в миллион раз больше просадит на каких-нибудь регекспах.
Для Боба важно понять принцип YAGNI (You aren't gonna need it).
Так, если разрабатывается бухгалтерское приложение, экономия памяти и времени "на спичках" не имеет никакого смысла.
В тоже самое время, если разрабатывается Core серверного решения с прицелом на высокую нагрузку, тогда "каждая спичка на особом счету".
Но! Если "каждая спичка на особом счету", тогда нужно кодить, обращаясь к памяти используя указатели, а не высокоуровневые функции.
Удачи, Роберт Мартин младший!
Стандартная библиотека содержит функцию FGETS, совершен- но аналогичную функции GETLINE, которую мы использовали на всем протяжении книги. В результате обращения FGETS(LINE, MAXLINE, FP) следующая строка ввода (включая символ новой строки) считы- вается из файла FP в символьный массив LINE; самое большое MAXLINE_1 символ будет прочитан. Результирующая строка за- канчивается символом \ 0. Нормально функция FGETS возвращает LINE; в конце файла она возвращает NULL. (Наша функция GETLINE возвращает длину строки, а при выходе на конец файла нуль). Предназначенная для вывода функция FPUTS записывает строку (которая не обязана содержать символ новой строки) в файл: FPUTS(LINE, FP)
Чтобы показать, что в функциях типа FGETS и FPUTS нет
ничего таинственного, мы приводим их ниже, скопированными непосредственно из стандартной библиотеки ввода-вывода: #INCLUDE <STDIO.H> CHAR *FGETS(S,N,IOP) /GET AT MOST N CHARS FROM IOP/ CHAR *S; INT N; REGISTER FILE *IOP; ( REGISTER INT C; REGISTER CHAR *CS; CS = S; WHILE(--N>0&&(C=GETC(IOP)) !=EOF) IF ((*CS++ = C)=='\N') BREAK; *CS = '\0'; RETURN((C==EOF && CS==S) 7 NULL : S); ) FPUTS(S,IOP) /PUT STRING S ON FILS IOP/ REGISTER CHAR *S; REGISTER FILE *IOP; ( REGISTER INT C; WHILE (C = *S++) PUTC(C,IOP); )
Керниган, Ричи. Язык программирования С
Не благодарите.
Пример как писать нормальный интерфейс.
Вышеприведенный код не призыв переписать все на С. А пример реализации такого же метода написанный давным давно людьми которые понимаю «в колбасных обрезках» сравните их метод и то что нагромоздили в статье.
Использование C++ должно упростить интерфейс - так как благодаря наличию конструктора /декструктора не надо явно высвобождать память. Вместо этого нам представили какой-то дичайший код, которым не ясно как пользоваться, прикрываясь заявлением о том что стандартный слишком медленный.
Пример как писать нормальный интерфейс.
... а также как писать комментарий на хабре так, что его чёрта с два прочитаешь.

Я правильно понимаю, что та память, на которую указывает этот string_view, может быть перезатёрта в последующих вызовах readline(), то есть строку нужно или сразу анализировать, или сразу копировать?
Боб ещё забыл об асинхронности. Но самое главное, он забыл произвести предварительную оценку того, стоит ли овчинка выделки. А начинать следовало именно с этого.
Как Боб текстовый файл считывал