Программа частоты слов - входной файл слишком большой?

Я все еще работаю над проблемой, упомянутой в этом сообщении: Sorting вектор строк с начальными числами

Исходная проблема заключается в следующем:

Напишите полную программу на C++, которая выводит k наиболее часто используемых слов в файле input.txt, по одному в строке в порядке убывания частоты, где k — неотрицательное целое число, считываемое из ввода. Связи разрываются произвольным образом, и если в input.txt есть только u разных слов и u ‹ k, то в выходных данных будет только u записей. Для этой задачи вы не можете использовать какой-либо класс или алгоритм STL, кроме вектора и строки. Слово — это максимальный блок непробельных символов с удаленными знаками препинания. Каждая выходная строка состоит из слова, за которым следует его частота. (входы и k-значения даны)

Благодаря тем, кто предложил использовать структуру, я получил немного более эффективное решение с меньшим количеством кода.

Однако проблема в том, что для относительно больших текстовых файлов (состоящих из >400000 слов) моя программа может работать более 5 минут и не дает никакого результата. Программа отлично работает на небольших входных файлах. Я не уверен, связано ли это с тем, что файл был слишком большим, или с самим алгоритмом связана проблема, которая вызывает переполнение/повреждение памяти.

Вот мой код программы:

struct word_freq {
int freq;
string word;
};

bool operator<(const word_freq& a, const word_freq& b) {
    return a.freq < b.freq;
}
void word_frequencies(ifstream& inf, int k)
{
vector <string> input;
string w;
while (inf >> w)
{
    remove_punc(w);
    input.push_back(w);
}
sort(input.begin(), input.end());

// initialize frequency vector
vector <int> freq;
for (size_t i = 0; i < input.size(); ++i) freq.push_back(1);

// count actual frequencies
int count = 0;
for (size_t i = 0; i < input.size()-1; ++i)
{
    if (input[i] == input[i+1])
    {
        ++count;
    } else
    {
        freq[i] += count;
        count = 0;
    }
}

// words+frequencies
vector <word_freq> wf;
for (int i = 0; i < freq.size(); ++i)
{
    if (freq[i] > 1 || is_unique(input, input[i]))
    {
        word_freq st = {freq[i], input[i]};
        wf.push_back(st);
    }
}

// printing
sort(wf.begin(), wf.end());
if (wf.size() < k)
{
    for (int i = wf.size()-1; i >= 0; --i)
    {
        cout << wf[i].word << " " << wf[i].freq << endl;
    }
} else
{
    for (int i = wf.size()-1; i >= wf.size()-1-k; --i)
    {
        cout << wf[i].word << " " << wf[i].freq << endl;
    }
}
}

Если кто укажет на допущенные ошибки, буду очень признателен.


person merukii6912    schedule 11.05.2017    source источник
comment
Отладчик точно скажет вам, что происходит. Дайте вашему приложению немного поработать, а затем приостановите его, чтобы посмотреть, что оно делает в данный момент.   -  person François Andrieux    schedule 11.05.2017
comment
Вы пытались выполнить код с помощью отладчика? Или прицепить к вашей программе отладчик, когда вы думаете, что она остановилась, чтобы исследовать ее?   -  person Algirdas Preidžius    schedule 11.05.2017
comment
Вы говорите, что не можете использовать алгоритмы или контейнеры, кроме std::vector или std::string, но вы используете std::sort(), что еще вы нам не говорите?   -  person Slava    schedule 11.05.2017
comment
valgrind также может быть полезен, если он зависает.   -  person didiz    schedule 11.05.2017
comment
Если вы хотите, чтобы люди помогали вам, лучше сначала помогите людям. Ваш код трудно читать.   -  person Slava    schedule 11.05.2017
comment
remove_punc - в вашем коде отсутствует определение этой функции.   -  person Snps    schedule 11.05.2017
comment
Если вы не можете использовать стандартную библиотеку (надеюсь, за исключением потоков), вам, предположительно, нужно либо реализовать свою собственную функцию сортировки, либо собственную хеш-карту, чтобы решить эту проблему (если вы не хотите идти O(n³+)). Не могу сказать, какой из них проще (вероятно, сортировать).   -  person Snps    schedule 11.05.2017
comment
Если отладчик изначально ничего не показывает, и вам не хочется выполнять миллион циклов чтения ваших больших данных, вы можете просто заставить свою программу выводить свой прогресс. Например, суммировать количество символов, обработанных на каждой итерации, разделить на общий размер файла в байтах (определенный в начале) и вывести процент выполнения. Также вы можете ограничить вывод этого значения каждой сотой итерацией или около того, используя, например,, if (count % 100 == 0). Таким образом, вы можете почувствовать, сколько времени это займет и кажется ли это разумным.   -  person Snps    schedule 11.05.2017
comment
я думаю, что @Slava что-то знает о sort(). ваш оператор sort((input.begin(), input.end()). может быть проблемой при распределении векторной памяти, где сортировка приводит к перераспределению элементов, не соответствующих заданному распределению   -  person Dr t    schedule 12.05.2017


Ответы (2)


Вы заставляете свою программу слишком сильно совпадать с памятью и вычислениями. Сначала вы читаете все слова в память и сортируете их. Затем вы вычисляете частоты и заполняете еще один вектор. У вас должно быть std::vector<word_freq> на первом месте, сортируйте его по словам (путем вставки элементов в нужное место) и вставляйте новый элемент или увеличивайте счетчик на существующем. Затем пересортируйте этот вектор по частотам и распечатайте.

Например, как вы могли бы переписать свой цикл:

struct word_freq {
    int freq;
    std::string word;

    word_freq( const std::string &w ) : word( w ), freq( 0 ) {}
};


void addWord( std::vector<word_freq> &v, const std::string &word )
{
     word_freq tmp( word );
     auto p = std::equal_range( v.begin(), v.end(), tmp, 
         []( const word_freq &w1, const word_freq &w2 ) {
             return w1.word < w2.word;
     } );
     if( p.first == p.second )  // not found
         p.first = v.insert( p.second, tmp ); // insert into proper place
     p.first->freq++; // increase freq counter
}

// ......
std::vector<word_freq> words;
string w;
while (inf >> w)
{
    remove_punc(w);
    addWord( words, w );
}
// here your vector sorted by words, there are no dups and counters have proper value already
// just resort it by freq and print

Подробности о том, как сохранить сортировку векторов, можно найти здесь как вы вставляете значение в отсортированный вектор?

С другой стороны, сохранение сортировки std::vector<word_freq> потребует слишком совпадающих вставок в середину или начало вектора, что может быть довольно дорогим и медленным. Поэтому, если вы реализуете описанную логику и заставляете ее работать на небольших примерах, и она все еще слишком медленная для вашего большого ввода - вы должны сортировать вектор индексов вместо вектора самого word_freq. При этом все равно потребуется вставить в начало или середину вектора целых чисел, но такая операция значительно дешевле и быстрее. Подробности о том, как индексы сортировки вместо самого вектора можно найти здесь: сравнить функцию сортировки в C++ для сортировки по индексу

person Slava    schedule 12.05.2017

Если вы используете reserve(int) после выделения векторов, производительность будет намного лучше.

Возвращение к векторам постоянно вызывает фрагментацию памяти.

Причина в том, что векторы постоянно перерастают свои выделенные границы и часто перераспределяются. Перераспределение небольших объектов часто обходится дорого и напрямую влияет на производительность.

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

Подробнее здесь:

Что такое фрагментация памяти?

И тут:

Должен ли я беспокоиться о фрагментации памяти с помощью std::vector?

Небольшая демонстрация с замерами производительности:

#include <chrono>
#include <vector>
#include <iostream>

int main()
{
        std::vector<std::string> slow;
        std::string d = "divide and conquer";

        std::chrono::time_point<std::chrono::system_clock> start, end;
        start = std::chrono::system_clock::now();

        // I get reallocated all the time
        for ( int i=0; i < 100000; i++ )
        {
            slow.push_back(d);
        }

        end = std::chrono::system_clock::now();

        std::chrono::duration<double> elapsed_seconds = end-start;
        std::time_t end_time = std::chrono::system_clock::to_time_t(end);

        std::cout << "elapsed time v1: " << elapsed_seconds.count() << "s\n";

        start = std::chrono::system_clock::now();

        //I don't move around
        slow.reserve(100000);
        slow.clear();
        for ( int i=0; i < 100000; i++ )
        {
            slow.push_back(d);
        }

        end = std::chrono::system_clock::now();

        elapsed_seconds = end-start;
        end_time = std::chrono::system_clock::to_time_t(end);

        std::cout << "elapsed time v2: " << elapsed_seconds.count() << "s\n";
        return 0;
}

Вывод:

    elapsed time v1: 0.014085s

    elapsed time v2: 0.004597s
person didiz    schedule 11.05.2017
comment
Я не голосовал против, и я не согласен с вызовом резерва, если количество элементов известно заранее, но как в мире вектор, который гарантирует непрерывную память, вызывает фрагментацию памяти? - person Christopher Pisz; 11.05.2017
comment
@ChristopherPisz: Нажмите на предоставленную ссылку и узнайте - person Lightness Races in Orbit; 11.05.2017
comment
@BoundryImposition Я сделал, и в нем не было абсолютно ничего, что могло бы подтвердить такое утверждение. Я бы не стал комментировать, не прочитав сначала страницу, указанную по ссылке. Непрерывная память и фрагментация одновременно не имеют смысла. Можешь ли ты быть легким, будучи темным, и тяжелым, будучи легким? Вы не можете быть фрагментированы и непрерывны. Возможно, они имеют в виду память, отличную от той, что используется вектором, но опять же, никаких объяснений не дано. Так что, кто знает, о чем они говорят. - person Christopher Pisz; 11.05.2017
comment
@ChristopherPisz: Фрагментация происходит не из-за того, что хранилище является непрерывным, а из-за того, что вы постоянно перемещаете его, расширяя с частыми возвратами и без резерва (теоретически). Ответ Петра ссылается на хорошее объяснение того, как происходит фрагментация памяти. - person Lightness Races in Orbit; 11.05.2017
comment
@ChristopherPisz Простой пример: допустим, у вас есть общая память 10 байт. Вы собираетесь вставлять 8 байт один за другим в вектор. Если начать с использования резерва, вектор выделяет 8 байт, и все работает нормально. Если вы не резервируете, вектор начинает выделять, скажем, 6 байтов. Байты №1-6 проталкиваются нормально, а вот байт №7 не подходит. Вектор должен выделить другой блок, который может содержать старые данные плюс место для новых, то есть, 6+ байтов, чтобы он мог скопировать старые данные в новый блок перед освобождением старого блока памяти. Однако от общей памяти осталось всего 4 байта. - person Snps; 11.05.2017
comment
@snps, который на самом деле не имеет ничего общего с вектором, это произойдет со всеми распределениями в любое время и в любом месте. Было бы более понятно сказать, что для того, чтобы свести к минимуму выделения для всех контейнеров STL, можно использовать резерв, а затем специально вызывать вектор. - person Christopher Pisz; 11.05.2017
comment
Извините, что не объясняю больше, я всегда предпочитаю добавлять ссылку, которая объясняет лучше, чем я могу, в любом случае я написал небольшую программу, демонстрирующую, что я имею в виду для OP, чтобы повысить производительность, при 400K слов нужно думать о фрагментации и это может быть его единственная проблема. - person didiz; 11.05.2017
comment
@ChristopherPisz Конечно, другие типы распределения в любом месте также могут вызывать фрагментацию, но аргумент здесь был в том, вызывает ли фрагментация расширение std::vector. Вы также, кажется, утверждаете, что непрерывное выделение памяти не может вызвать фрагментацию. Это просто неправильно. Возможно, вы неправильно поняли, что фрагментируется не сама непрерывная часть памяти, а общий пул памяти. - person Snps; 11.05.2017
comment
@ChristopherPisz И, чтобы прояснить, я не говорю, что фрагментация памяти будет большой проблемой в этой конкретной проблеме в целом. Я как бы присоединился к этому спору на эту конкретную тему. - person Snps; 11.05.2017
comment
@snps Давай! Спасибо за ясность. - person Christopher Pisz; 11.05.2017
comment
Я сомневаюсь, что фрагментация является проблемой и даже единственной проблемой. Основная проблема OP не должна считывать все слова в вектор и сортировать, но должна сохранять сортировку векторов без дубликатов и увеличивать счетчик. Но трудно сказать, может ли OP использовать std::lower_bound или std::equal_range или должен это реализовать. - person Slava; 12.05.2017