Предложения по алгоритму поиска дубликатов файлов (с использованием C) - PullRequest
2 голосов
/ 18 апреля 2010

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

Моя первая идея состояла в том, чтобы «разбить» файлы на блоки фиксированного размера, затем запустить поток для каждого блока, перейти к символу запуска каждого блока и продолжить сравнение параллельно. При сбое сравнения из потока другие рабочие потоки отменяются, и программа выходит из цикла порождения потока.

Код выглядит так: dupf.h

#ifndef __NM__DUPF__H__
#define __NM__DUPF__H__
#define NUM_THREADS 15
#define BLOCK_SIZE 8192

/* Thread argument structure */
struct thread_arg_s {
    const char *name_f1;        /* First file name */
    const char *name_f2;        /* Second file name */
    int cursor;                 /* Where to seek in the file */
};
typedef struct thread_arg_s thread_arg;

/**
 * 'arg' is of type thread_arg.
 * Checks if the specified file blocks are 
 * duplicates.
 */
void *check_block_dup(void *arg);

/**
 * Checks if two files are duplicates
 */
int check_dup(const char *name_f1, const char *name_f2);

/**
* Returns a valid pointer to a file.
* If the file (given by the path/name 'fname') cannot be opened
* in 'mode', the program is interrupted an error message is shown.
**/
FILE *safe_fopen(const char *name, const char *mode);

#endif

dupf.c

#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "dupf.h"

FILE *safe_fopen(const char *fname, const char *mode)
{
    FILE *f = NULL;
    f = fopen(fname, mode);
    if (f == NULL) {
        char emsg[255];
        sprintf(emsg, "FOPEN() %s\t", fname);
        perror(emsg);
        exit(-1);
    }
    return (f);
}

void *check_block_dup(void *arg)
{
    const char *name_f1 = NULL, *name_f2 = NULL;    /* File names */
    FILE *f1 = NULL, *f2 = NULL;                    /* Streams */
    int cursor = 0;                                 /* Reading cursor */
    char buff_f1[BLOCK_SIZE], buff_f2[BLOCK_SIZE];  /* Character buffers */
    int rchars_1, rchars_2;                         /* Readed characters */
    /* Initializing variables from 'arg' */
    name_f1 = ((thread_arg*)arg)->name_f1;
    name_f2 = ((thread_arg*)arg)->name_f2;
    cursor = ((thread_arg*)arg)->cursor;
    /* Opening files */
    f1 = safe_fopen(name_f1, "r");
    f2 = safe_fopen(name_f2, "r");
    /* Setup cursor in files */
    fseek(f1, cursor, SEEK_SET);
    fseek(f2, cursor, SEEK_SET);
    /* Initialize buffers */
    rchars_1 = fread(buff_f1, 1, BLOCK_SIZE, f1);
    rchars_2 = fread(buff_f2, 1, BLOCK_SIZE, f2);
    if (rchars_1 != rchars_2) {
        /* fread failed to read the same portion.
         * program cannot continue */
        perror("ERROR WHEN READING BLOCK");
        exit(-1);
    }
    while (rchars_1-->0) {
        if (buff_f1[rchars_1] != buff_f2[rchars_1]) {
            /* Different characters */
            fclose(f1);
            fclose(f2);
            pthread_exit("notdup");
        }
    }
    /* Close streams */
    fclose(f1);
    fclose(f2);
    pthread_exit("dup");
}

int check_dup(const char *name_f1, const char *name_f2)
{
    int num_blocks = 0;             /* Number of 'blocks' to check */
    int num_tsp = 0;                /* Number of threads spawns */
    int tsp_iter = 0;               /* Iterator for threads spawns */
    pthread_t *tsp_threads = NULL;
    thread_arg *tsp_threads_args = NULL;
    int tsp_threads_iter = 0;
    int thread_c_res = 0;           /* Thread creation result */
    int thread_j_res = 0;           /* Thread join res */
    int loop_res = 0;               /* Function result */
    int cursor;
    struct stat buf_f1;
    struct stat buf_f2;

    if (name_f1 == NULL || name_f2 == NULL) {
        /* Invalid input parameters */
        perror("INVALID FNAMES\t");
        return (-1);
    }

    if (stat(name_f1, &buf_f1) != 0 || stat(name_f2, &buf_f2) != 0) {
        /* Stat fails */
        char emsg[255];
        sprintf(emsg, "STAT() ERROR: %s %s\t", name_f1, name_f2);
        perror(emsg);
        return (-1);
    }

    if (buf_f1.st_size != buf_f2.st_size) {
        /* File have different sizes */
        return (1);
    }

    /* Files have the same size, function exec. is continued */
    num_blocks = (buf_f1.st_size / BLOCK_SIZE) + 1;
    num_tsp = (num_blocks / NUM_THREADS) + 1;
    cursor = 0;
    for (tsp_iter = 0; tsp_iter < num_tsp; tsp_iter++) {
        loop_res = 0;
        /* Create threads array for this spawn */
        tsp_threads = malloc(NUM_THREADS * sizeof(*tsp_threads));
        if (tsp_threads == NULL) {
            perror("TSP_THREADS ALLOC FAILURE\t");
            return (-1);
        }
        /* Create arguments for every thread in the current spawn */
        tsp_threads_args = malloc(NUM_THREADS * sizeof(*tsp_threads_args));
        if (tsp_threads_args == NULL) {
            perror("TSP THREADS ARGS ALLOCA FAILURE\t");
            return (-1);
        }
        /* Initialize arguments and create threads */
        for (tsp_threads_iter = 0; tsp_threads_iter < NUM_THREADS;
                tsp_threads_iter++) {
            if (cursor >= buf_f1.st_size) {
                break;
            }
            tsp_threads_args[tsp_threads_iter].name_f1 = name_f1;
            tsp_threads_args[tsp_threads_iter].name_f2 = name_f2;
            tsp_threads_args[tsp_threads_iter].cursor = cursor;
            thread_c_res = pthread_create(
                               &tsp_threads[tsp_threads_iter],
                               NULL,
                               check_block_dup,
                               (void*)&tsp_threads_args[tsp_threads_iter]);
            if (thread_c_res != 0) {
                perror("THREAD CREATION FAILURE");
                return (-1);
            }
            cursor+=BLOCK_SIZE;
        }
        /* Join last threads and get their status */
        while (tsp_threads_iter-->0) {
            void *thread_res = NULL;
            thread_j_res = pthread_join(tsp_threads[tsp_threads_iter],
                                        &thread_res);
            if (thread_j_res != 0) {
                perror("THREAD JOIN FAILURE");
                return (-1);
            }
            if (strcmp((char*)thread_res, "notdup")==0) {
                loop_res++;
                /* Closing other threads and exiting by condition
                 * from loop. */
                while (tsp_threads_iter-->0) {
                    pthread_cancel(tsp_threads[tsp_threads_iter]);
                }
            }
        }
        free(tsp_threads);
        free(tsp_threads_args);
        if (loop_res > 0) {
            break;
        }
    }
    return (loop_res > 0) ? 1 : 0;
}

Функция отлично работает (по крайней мере, для того, что я тестировал). Тем не менее, некоторые ребята из #C (freenode) предположили, что решение слишком сложное и может работать плохо из-за параллельного чтения на hddisk.

Что я хочу знать:

  • Является ли многопоточный подход некорректным по умолчанию?
  • fseek () такой медленный?
  • Есть ли способ как-то отобразить файлы в память и затем сравнить их?

РЕДАКЦИЯ С ПОЛОЖЕНИЕМ:

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

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

Вот новая реализация (очень грубый и прямой код):

#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "dupf.h"

/**
* Safely assures that a file is opened. 
* If cannot open file, the flow of the program is interrupted.
* The error code returned is -1.
**/
FILE *safe_fopen(const char *fname, const char *mode)
{
    FILE *f = NULL;
    f = fopen(fname, mode);
    if (f == NULL) {
        char emsg[1024];
        sprintf(emsg, "Cannot open file: %s\t", fname);
        perror(emsg);
        exit(-1);
    }
    return (f);
}

/**
* Check if two files have the same size.
* Returns:
* -1    Error.
* 0 If they have the same size.
* 1 If the don't have the same size.
**/
int check_same_size(const char *f1_name, const char *f2_name, off_t *f1_size, off_t *f2_size)
{
    struct stat f1_stat, f2_stat;
    if((f1_name == NULL) || (f2_name == NULL)){
        fprintf(stderr, "Invalid filename passed to function [check_same_size].\n");
        return (-1);
    }
    if((stat(f1_name, &f1_stat) != 0) || (stat(f2_name, &f2_stat) !=0)){
        fprintf(stderr, "Cannot apply stat. [check_same_size].\n");
        return (-1);
    }
    if(f1_size != NULL){
        *f1_size = f1_stat.st_size;
    }
    if(f2_size != NULL){
        *f2_size = f2_stat.st_size;
    }
    return (f1_stat.st_size == f2_stat.st_size) ? 0 : 1;
}

/**
* Test if two files are duplicates.
* Returns:
* -1    Error.
* 0 If they are duplicates.
* 1 If they are not duplicates.
**/
int check_dup_plain(char *f1_name, char *f2_name, int block_size)
{
    if ((f1_name == NULL) || (f2_name == NULL)){
        fprintf(stderr, "Invalid filename passed to function [check_dup_plain].\n");
        return (-1);
    }
    FILE *f1 = NULL, *f2 = NULL;
    char f1_buff[block_size], f2_buff[block_size];
    size_t rch1, rch2;
    if(check_same_size(f1_name, f2_name, NULL, NULL) == 1){
        return (1);
    }
    f1 = safe_fopen(f1_name, "r");
    f2 = safe_fopen(f2_name, "r");
    while(!feof(f1) && !feof(f2)){
        rch1 = fread(f1_buff, 1, block_size, f1);
        rch2 = fread(f2_buff, 1, block_size, f2);
        if(rch1 != rch2){
            fprintf(stderr, "Invalid reading from file. Cannot continue. [check_dup_plain].\n");
            return (-1);
        }
        while(rch1-->0){
            if(f1_buff[rch1] != f2_buff[rch1]){
                return (1);
            }
        }
    }
    fclose(f1);
    fclose(f2);
    return (0);
}

/**
* Test if two files are duplicates.
* Returns:
* -1    Error.
* 0 If they are duplicates.
* 1 If they are not duplicates.
**/
int check_dup_memmap(char *f1_name, char *f2_name)
{
    struct stat f1_stat, f2_stat;
    char *f1_array = NULL, *f2_array = NULL;
    off_t f1_size, f2_size;
    int f1_des, f2_des, cont, res;
    if((f1_name == NULL) || (f2_name == NULL)){
        fprintf(stderr, "Invalid filename passed to function [check_dup_memmap].\n");
        return (-1);    
    }
    if(check_same_size(f1_name, f2_name, &f1_size, &f2_size) == 1){
        return (1);
    }
    f1_des = open(f1_name, O_RDONLY);
    f2_des = open(f2_name, O_RDONLY);
    if((f1_des == -1) || (f2_des == -1)){
        perror("Cannot open file");
        exit(-1);       
    }
    f1_array = mmap(0, f1_size * sizeof(*f1_array), PROT_READ, MAP_SHARED, f1_des, 0);
    if(f1_array == NULL){
        fprintf(stderr, "Cannot map file to memory [check_dup_memmap].\n");
        return (-1);
    }
    f2_array = mmap(0, f2_size * sizeof(*f2_array), PROT_READ, MAP_SHARED, f2_des, 0);
    if(f2_array == NULL){
        fprintf(stderr, "Cannot map file to memory [check_dup_memmap].\n");
        return (-1);
    }
    cont = f1_size;
    res = 0;
    while(cont-->0){
        if(f1_array[cont]!=f2_array[cont]){
            res = 1;
            break;
        }
    }
    munmap((void*) f1_array, f1_size * sizeof(*f1_array));
    munmap((void*) f2_array, f2_size * sizeof(*f2_array));
    return res;
}

int main(int argc, char *argv[])
{
    printf("result: %d\n",check_dup_memmap("f2","f1"));
    return (0);
}

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

Спасибо за ваши ответы.

Ответы [ 7 ]

5 голосов
/ 18 апреля 2010

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

if fileA.size == fileB.size
    if fileA.hash() == fileB.hash()
        flag(fileA, fileB, same);

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

5 голосов
/ 18 апреля 2010

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

3 голосов
/ 18 апреля 2010

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

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

Если время, необходимое для вычисления «одинаковости» фрагмента, быстро по сравнению со временем, которое требуется для чтения этого фрагмента с диска, наличие отдельного потока мало поможет со второго (или третьего ...) поток будет тратить большую часть своего времени на ожидание завершения ввода-вывода.

Другим фактором является размер кэша процессора. Если вся память, которую вы обрабатываете за один раз, помещается в кэш-память ЦП, все будет гораздо быстрее, чем если бы разные потоки вызывали загрузку разных кусков памяти в кеш при выполнении инструкций.

Если у вас больше потоков, чем у ядер ЦП, вы просто замедляете процесс, делая ненужные переключатели контекста (поскольку потоку требуется ядро ​​для запуска).

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

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

2 голосов
/ 18 апреля 2010

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

Есть ли основания полагать, что сканирование файлов по частям позволит найти различия быстрее, чем сразу? Находятся ли данные, содержащиеся в файлах, преимущественно в определенном формате, и если да, приспособлена ли для них схема разбиения? Если нет, я не вижу, как сканирование файлов путем пропуска через каждые n байтов (что эффективно делает многопоточное разбиение) может предложить какое-либо улучшение по сравнению с чтением байтов в порядке их расположения на диске.

Подумайте о двух ограничивающих случаях - "разбить" файл на один блок и разбить файл на столько однобайтовых "блоков", сколько в файле байтов. Будет ли один из этих случаев более эффективным, чем другой, или какая-то промежуточная стоимость? Если нет промежуточного значения, которое вы должны оптимизировать, тогда вы ничего не знаете о том, как данные хранятся в файлах, поэтому не имеет значения, как вы их сканируете.

Даже если вы установите разделение для оптимизации производительности диска, например, размера блока, вам все равно придется вернуться, чтобы прочитать следующий байт, который, вероятно, будет находиться в крайне неоптимальной позиции. И, в конце концов, вам придется читать каждый байт в файле, независимо от того, как вы его разбили.

1 голос
/ 18 апреля 2010

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

Существуют некоторые среды, которые могут распределять рабочую нагрузку по ядрам, но даже в этом случае ЦП сможет обрабатывать намного быстрее, чем данные могут быть извлечены с диска, что система дискового ввода-вывода будет ограничивающим фактором .

1 голос
/ 18 апреля 2010

Ну, есть стандартная функция отображения карты mmap (), которая отображает файл в память. Вы должны быть в состоянии сделать что-то вроде

int fd1;
int fd2;
int size1;
int size2;

fd1 = open(name1, O_RDONLY);
size1 = lseek(fd1, 0, SEEK_END); 

fd2 = open(name2, O_RDONLY);
size2 = lseek(fd2, 0, SEEK_END);

if ( size1 == size2 )
{
   char * data1 = mmap(0, size1, PROT_READ, MAP_SHARED, fd1, 0);
   char * data2 = mmap(0, size1, PROT_READ, MAP_SHARED, fd2, 0);
   int i;

   /* ...and this is, obviously, where you'd do something more clever */
   for ( i = 0; i < size1 && *data1 == *data2; i++, data1++, data2++ );

   if ( i == size1 )
       printf("Equal\n");
}

close(fd1);
close(fd2);

Кроме этого, да, ваше решение выглядит слишком сложным ;-) Потоковый подход не обязательно ошибочен, но вы можете не увидеть, что параллельный доступ повышает производительность. Для дисков SAN или виртуальных дисков это может повысить производительность, для дисков с обычным вращающимся диском это может помешать. Но проще, как правило, лучше, если у вас действительно нет проблем с производительностью.

Относительно fseek () по сравнению с другими методами, это зависит от используемой операционной системы. Google - ваш друг, вы можете легко найти статьи по крайней мере для Solaris и Linux.

1 голос
/ 18 апреля 2010

Поскольку вы используете pthreads, я предполагаю, что вы работаете в среде Unix - в этом случае вы можете mmap (2) оба файла в памяти и напрямую сравнивать массивы памяти.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...