Импорт данных 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()
неоднократно, но если оптимизатор выполняет хорошую работу, которая должна быть уменьшена до незначительности. Как и при большой настройке производительности, вы часто не можете рассуждать о совершенстве и делать то, что будет самым быстрым, и вам придется попробовать альтернативы и измерить их с помощью вашего фактического компилятора, данных программы и т.д.