@yurchik322

Как ускорить работу скрипта?

Есть у меня данный код для построчной проверки двух файлов и записи одинаковых строк в двух файлах в отдельную папку, но он слишком медленный. Есть два тхт файла по 1 гб. И выполнение данной задачи этим скриптом занимает +-8 часов. Многовато, как можно ускорить?
#include <iostream>
#include <fstream>
#include <vector>
#include <string>


int main() {
    std::string path_first;
    std::string path_second;
    std::string path_result;

    std::cout << "First file path: ";
    std::getline(std::cin, path_first);

    std::cout << "Second file path: ";
    std::getline(std::cin, path_second);

    std::cout << "Result pass: ";
    std::getline(std::cin, path_result);


    std::ifstream file_first(path_first);
    std::ifstream file_second(path_second);
    std::ofstream file_result(path_result);

    if (!file_first.is_open() or !file_second.is_open() or !file_result.is_open()) {
        std::cout << "Invalid jobaniy blyat." << std::endl;
    }
    else {
        std::vector<std::string> lines_first;
        std::vector<std::string> lines_second;

        std::string buff;

        while (std::getline(file_first, buff)) lines_first.push_back(buff);
        while (std::getline(file_second, buff)) lines_second.push_back(buff);

        for (auto line_first : lines_first) {
            for (auto line_second : lines_second) {
                if (line_first == line_second) file_result << line_first << "\n";
            }
        }
    }

    file_first.close();
    file_second.close();
    file_result.close();

    return 0;
}
  • Вопрос задан
  • 128 просмотров
Решения вопроса 1
@kamenyuga
Насколько же неподходящий алгоритм надо использовать, чтобы 2 гигабайта данных 8 часов обрабатывать? Это ж задачка на считанные минуты и секунды.
Не нужно использовать вложенные циклы по двум векторам, это крайне неэффективно. Решение - бинарный поиск по отсортированным данным (multiset).
В идеале, конечно, уникализировать данные в обоих файлах, но это уже исходя из логики сравнения, добавляется элементарно (unordered_set).

Python (1 минута)
import datetime
import functools
import pathlib
import random
import time

import pandas as pd


def measure_and_log_running_time(func):

    @functools.wraps(func)
    def wrapper(*args, **kwargs):

        # вытаскиваем логгер из именованных аргументов функции
        logger = kwargs.get('logger')

        # замеряем время работы
        t_1 = time.time()
        ans = func(*args, **kwargs)
        running_time = datetime.timedelta(seconds=int(time.time() - t_1))

        # логируем время работы
        if logger:
            logger.info(f"{func.__name__}() running time {running_time}")
        else:
            print(f"{func.__name__}() running time {running_time}")

        return ans

    return wrapper


def format_size_in_bytes_with_si_prefix(value, *, unit_full=False, prefix_full=False):

    unit = ('B', 'byte')

    prefix_list = (
        ('', ''),
        ('K', 'kilo'),
        ('M', 'mega'),
        ('G', 'giga'),
        ('T', 'tera'))

    idx = 0
    while value >= 1024:
        idx += 1
        value /= 1024

    return f"{value:.2f} {prefix_list[idx][int(prefix_full)]}{unit[int(unit_full)]}"


@measure_and_log_running_time
def generate_lines(n):

    lines = list()

    for dummy in range(n):
        line_size = random.randrange(10, 100)
        lines.append(f"{random.randrange(16 ** line_size):0{line_size}x}\n")

    return lines


@measure_and_log_running_time
def write_lines(lines, filename):

    with open(filename, mode='w', encoding='UTF-8') as fout:
        fout.writelines(lines)

    return pathlib.Path(filename).stat().st_size


@measure_and_log_running_time
def generate_sample_data():

    print('a.txt')
    print(format_size_in_bytes_with_si_prefix(write_lines(generate_lines(20_000_000), 'a.txt')))

    print('b.txt')
    print(format_size_in_bytes_with_si_prefix(write_lines(generate_lines(20_000_000), 'b.txt')))


@measure_and_log_running_time
def process_sample_data(filename_1, filename_2, filename_3):

    df1 = pd.read_csv(filename_1, header=None, names=['single_data_column'])
    df2 = pd.read_csv(filename_2, header=None, names=['single_data_column'])
    pd.merge(
        df1,
        df2,
        how='inner',
        left_on=['single_data_column'],
        right_on=['single_data_column']
    ).to_csv(
        filename_3,
        index=False,
        header=False)


if __name__ == '__main__':

    # generate_sample_data()
    """
    a.txt
    generate_lines() running time 0:00:38
    write_lines() running time 0:00:05
    1.05 GB
    
    b.txt
    generate_lines() running time 0:00:38
    write_lines() running time 0:00:05
    1.05 GB
    
    generate_sample_data() running time 0:01:29
    """

    process_sample_data('a.txt', 'b.txt', 'c.txt')
    """
    process_sample_data() running time 0:00:45
    """


С++ (2 минуты)
#include <fstream>
#include <iostream>
#include <set>
#include <string>

#include "profiler.h"


void process_sample_data(
		const std::string& filename_1,
		const std::string& filename_2,
		const std::string& filename_3) {

	LOG_RUNNING_TIME(std::cout, "process_sample_data running time"); // profiler.h

	std::ifstream file_1(filename_1);
	std::ifstream file_2(filename_2);
	std::ofstream file_3(filename_3);

	std::multiset<std::string> lines_1, lines_2;
	std::string line;

	{
		LOG_RUNNING_TIME(std::cout, "file_1"); // profiler.h

		while (std::getline(file_1, line)) {
			lines_1.insert(line);
		}
	}

	{
		LOG_RUNNING_TIME(std::cout, "file_2"); // profiler.h

		while (std::getline(file_2, line)) {
			lines_2.insert(line);
		}
	}

	{
		LOG_RUNNING_TIME(std::cout, "comparison"); // profiler.h

		for (const auto& line_1 : lines_1) {
			if (lines_2.count(line_1) > 0) {
				file_3 << line_1 << "\n";
			}
		}
	}

	file_1.close();
	file_2.close();
	file_3.close();
}


int main() {

	process_sample_data(
		"C:/Users/fpn/PycharmProjects/nnf1_project/a.txt",
		"C:/Users/fpn/PycharmProjects/nnf1_project/b.txt",
		"C:/Users/fpn/PycharmProjects/nnf1_project/c.txt");

	/*
	file_1: 40958 ms
	file_2: 41392 ms
	comparison: 15027 ms
	process_sample_data running time: 118150 ms
	*/
	
    return 0;
}

"profiler.h"
#pragma once

#include <chrono>
#include <string>

using namespace std;
using namespace std::chrono;


#define UNIQ_ID_GLUE(x, y) x##y
#define UNIQ_ID_HELPER(prefix, id) UNIQ_ID_GLUE(prefix, id)
#define UNIQ_ID UNIQ_ID_HELPER(var_, __LINE__)
#define LOG_RUNNING_TIME(stream, message) RunningTimeLogger UNIQ_ID(stream, message)


class RunningTimeLogger {
public:
	explicit RunningTimeLogger(ostream& new_stream, const string& new_log_message)
	    	: stream(new_stream)
	    	, log_message(new_log_message)
			, start(steady_clock::now()) {
		// empty block of code
	}
	~RunningTimeLogger() {
		stream << log_message << (log_message.size() > 0 ? ": " : "")
			   << duration_cast<milliseconds>(steady_clock::now() - start).count() << " ms" << endl;
	}
private:
	ostream& stream;
	string log_message;
	steady_clock::time_point start;
};
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 2
wataru
@wataru Куратор тега C++
Разработчик на С++, экс-олимпиадник.
Вместо сравнения каждой строки с каждой используйте структуру данных trie. В стандартной библиотеке, к сожалению, ее нет. Поэтому придется писать самостоятельно. Там в каждой вершине храните сколько строчек в ней заканчиваются. Потом сложите туда все строчки из первого файла и проверяйте каждую строчку из второго и выводите ровно столько раз, сколько в первом файле она была.

Ну или можно использовать std::unordered_map<std::string, int> вместо trie.
Ответ написан
@res2001
Developer, ex-admin
Один из файлов (меньший) сразу читайте весь (блоками, об этом ниже) и каждую строку помещайте в std::unordered_set<std::string>.
Второй файл - построчно проверяете находится ли строка в set.

Читать файлы лучше не строками, а большими блоками (4Кб или больше). Наибольшее время занимают именно операции чтения файла. Читая файл большими блоками вы сокращаете количество операций чтения. Дальше работаете с блоком - вручную делите его на строки и т.д. Не забываете, что блок не обязан заканчиваться и начинаться ровно в конце или начале строки.

Если еще более оптимизировать, то std::unordered_set<std::string> лучше заменить на std::unordered_set<const char*> или std::unordered_set<std::string_view>.
Смысл в том, что std::string под каждую строку будет выделять динамическую память - выделение памяти само по себе медленная операции (конечно быстрее, чем чтение файла, но тем не менее), а тут вы сначала прочитаете кусок файла в большой блок который будет выделен в динамической памяти, а затем под каждую строку из этого блока будете еще раз выделять память. Поэтому лучше при чтении большими блоками первого файла выделять блок динамически и сразу прям внутри блока делить его на строки (т.е. добавлять символ '\0' в конце каждой строки) и указатель на начало каждой строки добавлять в set (или делать из него std::string_view и уже его добавлять в set).
Естественно выделенные блоки первого файла вам придется учитывать вручную (например хранить указатели на блоки в векторе) и после освобождения setа освобождать каждый блок (а не каждую строку).
Для второго файла будет достаточно одного блока.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы