Недавно автор узнал об инструменте csvquote, который кодирует проблемные символы CSV так, чтобы утилиты unix правильно их обрабатывали. Он меняет кодировку в конце конвейера, восстанавливая исходный ввод. Оригинальная реализация работает с кавычками CSV простым, примитивным методом. Но на современном «железе» есть подход лучше, проще и в 3 раза быстрее.  CSV часто используется как формат наборов данных. Подробностями делимся к старту флагманского курса по Data Science, который начнётся уже завтра. Есть ещё один подход, сочетающий принцип ОКМД (одиночный поток команд, множественный поток данных, также называется SIMD) с приёмами битового жонглирования, который на порядок увеличивает скорость обработки. Моя реализация — csvquote включает оба подхода. Записи в данных CSV разделяются символами переноса строки, а поля — запятыми. Поля могут заключаться в кавычки: Поля, содержащие символ переноса строки (U+000A), кавычки (U+0022) или запятую (U+002C), должны заключаться в кавычки, иначе они будут восприниматься как элементы форматирования CSV. Закавыченные кавычки превращаются в пару кавычек. Например, вот две записи с двумя полями в каждой: Инструмент, не поддерживающий применяемое в CSV разделение данных посредством запятых и символов переноса строки (например, awk), обработает эти записи некорректно.  Но csvquote переводит закавыченные символы переноса строки в разделители записей (U+001E), а запятые — в разделители элементов данных (U+001F). Эти управляющие символы редко появляются в обычных текстовых данных и могут быть легко обработаны в тексте в кодировке UTF-8 без декодирования или кодирования. Записи превращаются в: Управляющие символы здесь — \x1e и \x1f . Данные имеют ровно ту же длину, ведь это прямая замена байт на байт. Кавычки остаются нетронутыми. Задача — спарсить кавычки и отследить, окажутся ли два этих символа внутри пар кавычек или нет. Оригинальная реализация csvquote обходит входные данные побайтово и находится в одном из трёх состояний: Вне кавычек (исходное состояние). Внутри кавычек. В «сбежавшей» кавычке (первой " из ""). В моей реализации я использую конечный автомат вместе со switch: В реальной программе условий потенциальной замены больше. Убивающих производительность ветвлений ужасно много. Но в данном контексте мы имеем дело с поиском «снаружи-внутри», а не проверкой CSV, поэтому состояние в «сбежавшей» кавычке лишнее. Надо лишь сопоставить пары кавычек. «Сбежавшую» кавычку можно считать завершающей закавыченную область и сразу начинающей новую. А значит, в этой простой конфигурации есть только два первых состояния: Текст может обрабатываться в виде байтов, поэтому возможно только 256 вариантов входных данных. Имея 2 состояния и 256 входных данных, этот конечный автомат благодаря механизму замены может быть реализован без ветвлений с 512-байтовой таблицей. Вот инициализация таблицы: В первом состоянии символы отображаются сами на себя. Во втором — на свои замены. Вот энкодер и декодер целиком: Декодеру, строго говоря, не нужно обрабатывать кавычки. По бенчмарку (в моей реализации это csvdump) он на ноутбуке обрабатывает данные со скоростью ~1 ГБ/с — в 3 раза быстрее, чем в оригинальной реализации. И можно ускориться! В любой хорошей реализации ОКМД используется маскирование. Находим кавычки, вычисляем маску по закавыченным областям, и другую маску — для совпадений с заменами, объединяем эти маски, затем используем полученную маску для смешивания входных данных с заменами. Примерно так: Самое сложное — вычислить маску кавычек и как-то обработать закавыченные области, охватывающие фрагменты ОКМД, и сделать всё это без применения медленных побайтовых операций. К счастью, есть приёмы с битами, способные решить эти задачи. Вот что мы сделаем. Загрузим 32 байта в ОКМД-регистр (например, AVX2) и вычислим 32-битную маску, где каждый бит соответствует одному байту. Если в байте кавычка, соответствующий бит установлен в 1: Последний бит соответствует началу закавыченной области. Для маски желательно установить все биты, следующие за этим битом. Сделаем это, вычитая 1: Используя метод Кернигана, удалим этот бит из исходного ввода, выполнив операцию «И» для всех битов: Остался новый младший бит. Повторив операцию, создадим слои масок — по одному для каждой кавычки ввода: Видели исключающее «ИЛИ» в конечном автомате для переключения между состояниями в коде выше? Выполнив операцию исключающего «ИЛИ» для всех их вместе, мы включаем и выключаем кавычки, создавая закавыченные области: Но крайне важно, чтобы открывающая кавычка была включена в эту маску (скоро объясню почему). Выполнив операцию исключающего «ИЛИ» для предварительно вычитаемого значения с маской при вычислении маски, включаем и выключаем оставшиеся кавычки так, чтобы открывающие кавычки были включены. Вот соответствующая функция: Она даёт именно то, что нужно: Важно, чтобы открывающая кавычка была включена, ведь в области, начинающейся в последнем байте, будет этот последний набор битов. Используем этот последний бит, чтобы определить, начинается ли следующий фрагмент в закавыченном состоянии. Если это так, то нужно только выполнить логическую операцию «НЕ» для всего результата, чтобы изменить закавыченные области. Как добавить знак к 1 в набор всех битов или ничего не сделать для нуля? Выполнить логическую операцию «НЕ»: Так вычислятся закавыченные области, образовав цепочку между фрагментами. К сожалению, цикл приведёт к замедлению работы механизма предсказания ветвлений, если на входе будет много кавычек. Решить эту проблему не удалось. Однако допущена серьёзная ошибка. Я использую _mm256_movemask_epi8, а он помещает первый байт в младший бит! Вот как это выглядит: Эффективного способа перевернуть биты нет, поэтому нужно просто поработать в другом направлении. Чтобы перевернуть биты влево от установленного бита, выполняем логическую операцию «НЕ»: На этот раз исходный набор битов сохраняется. Поэтому, чтобы перевернуть кавычки, нужно выполнить операцию исключающего «ИЛИ» для исходного значения во входных данных. Это так же просто, как инициализация входными данными, а не нулём. И вот новый цикл: Результат: Теперь перенос зависит от старшего бита, а не от младшего: Следующая проблема: по непонятным причинам AVX2 не включает инверсию _mm256_movemask_epi8. Чтобы преобразовать битовую маску обратно в байтовую, надо кое-что перетасовать. К счастью, я не первый сталкиваюсь с этой проблемой, поэтому не пришлось решать её с нуля. Сначала заполняем 32-байтовый регистр повторяющимися копиями 32-битной маски: Перетасуем байты так, чтобы первые 8 байтов регистра имели одну и ту же копию первого байта битовой маски и т. д.: В байте 0 нас волнует только бит 0, в байте 1 — бит 1... в байте N — только бит N%8. Заранее вычислим маску, чтобы изолировать каждый из этих битов и сделать из битовой маски правильную побайтовую маску. К счастью, всё не так плохо, как я думал: вместо одной инструкции получилось 4. В моём бенчмарке встречаются случайно встречающиеся закавыченные поля, версия ОКМД обрабатывает данные со скоростью ~4 ГиБ/с — в 10 раз быстрее, чем в оригинальной реализации. Код не профилировался, но надеюсь, что ошибочные предсказания ветвлений в цикле битовой маски — это основная трудность, препятствующая гипотетическому ускорению в 32 раза. Также в моей версии дополнительно отклоняются входные данные, содержащие два специальных управляющих символа, так как кодировка будет необратимой. По мере возможности это реализовано в ОКМД, но замедляет обработку примерно на 10%. Джефф Лэнгдейл и другие любезно указали на PCLMULQDQ, который может вычислять маски кавычек с использованием умножения без переноса (ещё ссылка) полностью в ОКМД и без цикла. Я ещё не разобрался, как именно его применять, но он должен быть намного быстрее. Продолжить работу с данными и прокачать ваши навыки вы сможете на наших курсах. Профессия Data Scientist (24 месяца) Курс «Алгоритмы и структуры данных» (5 месяцев) Узнайте подробности акции. Data Science и Machine Learning Профессия Data Scientist Профессия Data Analyst Курс «Математика для Data Science» Курс «Математика и Machine Learning для Data Science» Курс по Data Engineering Курс «Machine Learning и Deep Learning» Курс по Machine Learning Python, веб-разработка Профессия Fullstack-разработчик на Python Курс «Python для веб-разработки» Профессия Frontend-разработчик Профессия Веб-разработчик Мобильная разработка Профессия iOS-разработчик Профессия Android-разработчик Java и C# Профессия Java-разработчик Профессия QA-инженер на JAVA Профессия C#-разработчик Профессия Разработчик игр на Unity От основ — в глубину Курс «Алгоритмы и структуры данных» Профессия C++ разработчик Профессия Этичный хакер А также Курс по DevOps Все курсы