Импорт данных ASCII: как я могу сопоставить производительность чтения Fortran в С++?

Настройка

Здравствуйте, у меня есть код Fortran для чтения в данных двойной точности ASCII (пример файла данных в нижней части вопроса):

program ReadData
    integer :: mx,my,mz
    doubleprecision, allocatable, dimension(:,:,:) :: charge

    ! Open the file 'CHGCAR'
    open(11,file='CHGCAR',status='old')

    ! Get the extent of the 3D system and allocate the 3D array
    read(11,*)mx,my,mz
    allocate(charge(mx,my,mz) )

    ! Bulk read the entire block of ASCII data for the system
    read(11,*) charge
end program ReadData

и "эквивалентный" код С++:

#include <fstream>
#include <vector>

using std::ifstream;
using std::vector;
using std::ios;

int main(){
    int mx, my, mz;

    // Open the file 'CHGCAR'
    ifstream InFile('CHGCAR', ios::in);

    // Get the extent of the 3D system and allocate the 3D array
    InFile >> mx >> my >> mz;
    vector<vector<vector<double> > > charge(mx, vector<vector<double> >(my, vector<double>(mz)));

    // Method 1: std::ifstream extraction operator to double
    for (int i = 0; i < mx; ++i)
        for (int j = 0; j < my; ++j)
            for (int k = 0; k < mz; ++k)
                InFile >> charge[i][j][k];

    return 0;
}

Фортран, пиная @$$ и принимая имена

Обратите внимание, что строка

read(11,*) charge

выполняет ту же задачу, что и код С++:

for (int i = 0; i < mx; ++i)
    for (int j = 0; j < my; ++j)
        for (int k = 0; k < mz; ++k)
            InFile >> charge[i][j][k];

где InFile - объект if stream (обратите внимание, что хотя итераторы в коде Fortran начинаются с 1, а не 0, диапазон один и тот же).

Однако, код Fortran работает намного быстрее, чем код на С++, я думаю, потому что Fortran делает что-то умное, как чтение/разбор файла в соответствии с диапазоном и формой (значения mx, my, mz) за один раз, а затем просто указав charge на память, на которую были прочитаны данные. Для сравнения, С++-код должен получить доступ к InFile, а затем charge (который обычно является большим) назад и вперед с каждой итерацией, в результате чего (я считаю) происходит много операций ввода-вывода и памяти.

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

Мой вопрос:

Как я могу добиться производительности кода Fortran на С++?

Перемещение...

Вот намного быстрее (чем выше С++) реализация С++, где файл читается за один проход в массив char, а затем charge заселяется при анализе массива char:

#include <fstream>
#include <vector>
#include <cstdlib>

using std::ifstream;
using std::vector;
using std::ios;

int main(){
    int mx, my, mz;

    // Open the file 'CHGCAR'
    ifstream InFile('CHGCAR', ios::in);

    // Get the extent of the 3D system and allocate the 3D array
    InFile >> mx >> my >> mz;
    vector<vector<vector<double> > > charge(mx, vector<vector<double> >(my, vector<double>(mz)));

    // Method 2: big char array with strtok() and atof()

    //  Get file size
    InFile.seekg(0, InFile.end);
    int FileSize = InFile.tellg();
    InFile.seekg(0, InFile.beg);

    //  Read in entire file to FileData
    vector<char> FileData(FileSize);
    InFile.read(FileData.data(), FileSize);
    InFile.close();

    /*
     *  Now simply parse through the char array, saving each
     *  value to its place in the array of charge density
     */
    char* TmpCStr = strtok(FileData.data(), " \n");

    // Gets TmpCStr to the first data value
    for (int i = 0; i < 3 && TmpCStr != NULL; ++i)
        TmpCStr = strtok(NULL, " \n");

    for (int i = 0; i < Mz; ++i)
        for (int j = 0; j < My; ++j)
            for (int k = 0; k < Mx && TmpCStr != NULL; ++k){
                Charge[i][j][k] = atof(TmpCStr);
                TmpCStr = strtok(NULL, " \n");
            }

    return 0;
}

Опять же, это намного быстрее, чем простой >> метод на основе операторов, но все же значительно медленнее, чем версия Fortran - не говоря уже о гораздо большем количестве кода.

Как повысить производительность?

Я уверен, что метод 2 - это путь, если я сам его реализую, но мне любопытно, как я могу повысить производительность, чтобы соответствовать коду Fortran. Типы вещей, которые я рассматриваю и в настоящее время исследую:

  • Функции С++ 11 и С++ 14
  • Оптимизированная библиотека C или С++ для выполнения этого типа
  • Усовершенствования отдельных методов, используемых в методе 2
    • библиотека токенизации, например, в С++ String Toolkit Library вместо strtok()
    • более эффективное преобразование char в double чем atof()

Инструмент С++ String Toolkit

В частности, библиотека инструментов С++ String Toolkit примет FileData и разделители " \n" и предоставит мне объект токена строки (назовите его FileTokens, тогда цикл triple for будет выглядеть как

for (int k = 0; k < Mz; ++k)
    for (int j = 0; j < My; ++j)
        for (int i = 0; i < Mx; ++i)
            Charge[k][j][i] = FileTokens.nextFloatToken();

Это немного упростило бы код, но есть дополнительная работа по копированию (по существу) содержимого FileData в FileTokens, что может привести к гибели любой выгоды от использования метода nextFloatToken() (предположительно более эффективного, чем комбинация strtok()/atof()).

На странице С++ String Toolkit (StrTk) учебника по токенизатору (см. внизу вопроса) используется StrTk for_each_line(), который похож на мое желаемое приложение. Однако разница между случаями заключается в том, что я не могу предположить, сколько данных будет отображаться в каждой строке входного файла, и я не знаю достаточно о StrTk, чтобы сказать, является ли это жизнеспособным решением.

NOT DUPLICATE

Тема быстрого чтения данных ASCII в массив или структуру появилась раньше, но я просмотрел следующие сообщения, и их решения были недостаточными:

Примеры данных

Вот пример файла данных, который я импортирую. Данные ASCII разделяются пробелами и разрывами строк, как показано ниже:

 5 3 3
 0.23080516813E+04 0.22712439791E+04 0.21616898980E+04 0.19829996749E+04 0.17438686650E+04
 0.14601734127E+04 0.11551623512E+04 0.85678544224E+03 0.59238325489E+03 0.38232265554E+03
 0.23514479113E+03 0.14651943589E+03 0.10252743482E+03 0.85927499703E+02 0.86525872161E+02
 0.10141182750E+03 0.13113419142E+03 0.18057147781E+03 0.25973252462E+03 0.38303754418E+03
 0.57142097675E+03 0.85963728360E+03 0.12548019843E+04 0.17106124085E+04 0.21415379433E+04
 0.24687336309E+04 0.26588012477E+04 0.27189091499E+04 0.26588012477E+04 0.24687336309E+04
 0.21415379433E+04 0.17106124085E+04 0.12548019843E+04 0.85963728360E+03 0.57142097675E+03
 0.38303754418E+03 0.25973252462E+03 0.18057147781E+03 0.13113419142E+03 0.10141182750E+03
 0.86525872161E+02 0.85927499703E+02 0.10252743482E+03 0.14651943589E+03 0.23514479113E+03

Пример StrTk

Вот пример StrTk, упомянутый выше. В сценарии анализируется файл данных, содержащий информацию для 3D-сетки:

входные данные:

5
+1.0,+1.0,+1.0
-1.0,+1.0,-1.0
-1.0,-1.0,+1.0
+1.0,-1.0,-1.0
+0.0,+0.0,+0.0
4
0,1,4
1,2,4
2,3,4
3,1,4

код:

struct point
{
   double x,y,z;
};

struct triangle
{
   std::size_t i0,i1,i2;
};

int main()
{
   std::string mesh_file = "mesh.txt";
   std::ifstream stream(mesh_file.c_str());
   std::string s;
   // Process points section
   std::deque<point> points;
   point p;
   std::size_t point_count = 0;
   strtk::parse_line(stream," ",point_count);
   strtk::for_each_line_n(stream,
                          point_count,
                          [&points,&p](const std::string& line)
                          {
                             if (strtk::parse(line,",",p.x,p.y,p.z))
                                points.push_back(p);
                          });

   // Process triangles section
   std::deque<triangle> triangles;
   triangle t;
   std::size_t triangle_count = 0;
   strtk::parse_line(stream," ",triangle_count);
   strtk::for_each_line_n(stream,
                          triangle_count,
                          [&triangles,&t](const std::string& line)
                          {
                             if (strtk::parse(line,",",t.i0,t.i1,t.i2))
                                triangles.push_back(t);
                          });
   return 0;
}

Ответы

Ответ 1

Это...

vector<vector<vector<double> > > charge(mx, vector<vector<double> >(my, vector<double>(mz)));

... создает временный vector<double>(mz) со всеми значениями 0.0 и копирует его my раз (или, возможно, перемещает, то копирует my-1 раз с компилятором С++ 11, но мало отличается...) для создания временного vector<vector<double>>(my, ...), который затем копируется mx times (... как указано выше...) для инициализации всех данных. Вы все равно читаете данные по этим элементам - нет необходимости тратить время на его инициализацию. Вместо этого создайте пустой charge и используйте вложенные циклы в reserve() достаточно памяти для элементов, не заполняя их.

Затем проверьте, что вы компилируете с оптимизацией. Если вы находитесь, и вы все еще медленнее, чем FORTRAN, в заполняющих данные вложенных циклах попробуйте создать ссылку на вектор, о котором вы находитесь о .emplace_back, на:

for (int i = 0; i < mx; ++i)
    for (int j = 0; j < my; ++j)
    {
        std::vector<double>& v = charge[i][j];
        for (int k = 0; k < mz; ++k)
        {
            double d;
            InFile >> d;
            v.emplace_pack(d);
        }
    }

Это не поможет, если ваш оптимизатор проделал хорошую работу, но стоит попробовать как проверку работоспособности.

Если вы все еще медленнее - или просто хотите попробовать еще быстрее - вы можете попытаться оптимизировать парсинг чисел: вы говорите, что ваши данные все отформатированы ala 0.23080516813E+04 - с фиксированными размерами, которые вы можете легко подсчитать, сколько байты для чтения в буфер, чтобы дать вам приличное количество значений из памяти, затем для каждого из них вы можете запустить atol после ., чтобы извлечь 23080516813, а затем умножить его на 10 на мощность минус (11 (ваш номер цифр) минус 04): для скорости держите таблицу этих степеней десяти и индексируйте в нее с использованием извлеченного показателя (т.е. 4). (Примечание умножения, например, 1E-7, может быть быстрее, чем деление на 1E7 на большом количестве общего оборудования.)

И если вы хотите, чтобы это произошло, переключитесь на использование доступа к файлам с памятью. Стоит рассмотреть boost::mapped_file_source, поскольку он проще в использовании, чем даже POSIX API (не говоря уже о Windows) и переносится, но программирование непосредственно против OS API тоже не должно быть большой проблемой.

UPDATE - ответ на первый и второй комментарии

Пример использования форматирования памяти памяти:

#include <boost/iostreams/device/mapped_file.hpp>

boost::mapped_file_params params("dbldat.in");
boost::mapped_file_source file(params);
file.open();
ASSERT(file.is_open());
const char* p = file.data();
const char* nl = strchr(p, '\n');
std::istringstream iss(std::string(p, nl - p));
size_t x, y, z;
ASSERT(iss >> x >> y >> z);

Вышеприведенный файл отображает файл в память по адресу p, затем анализирует размеры из первой строки. Продолжайте синтаксический анализ фактических double представлений от ++nl и далее. Я упоминаю подход к этому выше, и вы обеспокоены изменением формата данных: вы можете добавить номер версии в файл, поэтому вы можете использовать оптимизированный синтаксический анализ до тех пор, пока номер версии не изменится, а затем вернется к чему-то универсальному для "неизвестного", файлов. Поскольку что-то общее, для представления в памяти с использованием int chars_to_skip; double my_double; ASSERT(sscanf(ptr, "%f%n", &my_double, &chars_to_skip) == 1); является разумным: см. sscanf docs here - вы можете указатель через данные chars_to_skip.

Затем вы предлагаете объединить решение reserve() с решением создания ссылки?

Да.

И (помилуй мое невежество), почему использование ссылки на charge[i][j] и v.emplace_back() будет лучше, чем charge[i][j].emplace_back()?

Это предположение состояло в том, чтобы убедиться в том, что компилятор не неоднократно оценивал charge[i][j] для каждого элемента, который был установлен: надеюсь, что он не изменит производительность, и вы можете вернуться к charge[i][j].emplace(), но IMHO стоит быстро проверить.

Наконец, я скептически отношусь к использованию пустого вектора и резервирования() в вершинах каждого цикла. У меня есть еще одна программа, которая пришла к остановке, используя этот метод, и заменив резерв() s на предварительно выделенный многомерный вектор, ускорило его.

Это возможно, но не обязательно верно вообще или применимо здесь - многое зависит от компилятора/оптимизатора (особенно для разворачивания цикла) и т.д. С неоптимизированным emplace_back вам нужно проверить вектор size() на capacity() неоднократно, но если оптимизатор выполняет хорошую работу, которая должна быть уменьшена до незначительности. Как и при большой настройке производительности, вы часто не можете рассуждать о совершенстве и делать то, что будет самым быстрым, и вам придется попробовать альтернативы и измерить их с помощью вашего фактического компилятора, данных программы и т.д.