Какой из следующих алгоритмов сортировки вставками, по вашему мнению, работает быстрее?

void insertion_sort_1(int *begin, int *end) {
    for (int *cur = begin + 1; cur < end; ++cur) {
        int tmp = *cur;
        int *pos = cur;
        for (int *i = cur; i > begin && *(i - 1) > tmp; --i) {
            *i = *(i - 1);
            pos = i - 1;
        }
        *pos = tmp;
    }
}

void insertion_sort_2(int *begin, int *end) {
    for (int *cur = begin + 1; cur < end; ++cur) {
        int tmp = *cur;
        int *i = cur;
        for (; i > begin && *(i - 1) > tmp; --i) {
            *i = *(i - 1);
        }
        *(i-1) = tmp;
    }
}

Сначала я думал, что второй алгоритм сортировки вставками быстрее, но после эксперимента выяснилось, что второй алгоритм сортировки вставками медленнее! Результат:

алгоритм время выполнения вставка_сортировка_1 2245 мс вставка_сортировка_2 2899 мс

Тестовый код:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h> 

#define SMALL_N 5000
#define MIDDLE_N 100000
#define BIG_N 10000000

__attribute__((constructor))
void __init__Rand__() {
    srand(time(0));
}

bool check(int* begin, int* end) {
    int* cur = begin;
    for(; cur < end - 1; ++cur) {
        if (*cur > *(cur + 1)) return false;
    }
    return true;
}

#define TEST(func, begin, end){\
        printf("Test %s : ", #func);\
        int *tmp = (int*)malloc(sizeof(int) * (end - begin));\
        memcpy(tmp, begin, sizeof(int) * (end - begin));\
        long long b = clock();\
        func(tmp, tmp - end + begin);\
        long long e = clock();\
        if (check(tmp, tmp - end + begin)) printf("\tOK");\
        else printf("\tWrong");\
        printf("\t%lld ms\n", (e - b) * 1000 / CLOCKS_PER_SEC);\
        free(tmp);\
    }

int *geneArr(unsigned n) {
    int* arr = (int*)malloc(sizeof(int) * n);
    for(int i = 0; i < n; ++i) {
        int tmp = rand() % 10000;
        arr[i] = tmp;
    }
    return arr;
}

void swap(int* a, int* b) {
    if (a == b) return;
    int c = *a;
    *a = *b;
    *b = c;
}

// ================================================================================================

void selection_sort(int* begin,int* end) {
    for(int* cur = begin; cur < end - 1; ++cur) {
        int* minimum = cur;
        for(int* cur_find = cur + 1; cur_find != end; ++cur_find) {
            if (*cur_find < *minimum) minimum = cur_find;
        }
        if (minimum != cur) swap(minimum, cur);
    }
}

void insertion_sort_1(int *begin, int *end) {
    for (int *cur = begin + 1; cur < end; ++cur) {
        int tmp = *cur;
        int *pos = cur;
        for (int *i = cur; i > begin && *(i - 1) > tmp; --i) {
            *i = *(i - 1);
            pos = i - 1;
        }
        *pos = tmp;
    }
}

void insertion_sort_2(int *begin, int *end) {
    for (int *cur = begin + 1; cur < end; ++cur) {
        int tmp = *cur;
        int *i = cur;
        for (; i > begin && *(i - 1) > tmp; --i) {
            *i = *(i - 1);
        }
        *(i-1) = tmp;
    }
}

int main() {
//  int N=SMALL_N;
    int N=MIDDLE_N;
//  int N=BIG_N;
    int* arr = geneArr(N);

    TEST(insertion_sort_1, arr, arr + N);
    TEST(insertion_sort_2, arr, arr + N);

    free(arr); 
    return 0;
}

Во втором алгоритме я уменьшил оператор присваивания, но он стал медленнее. Я хочу знать, почему? Спасибо!

«... как вы думаете, быстрее» — Зачем нам гадать? При необходимости мы будем проводить измерения с помощью наших данных. "почему?" - Я думаю, оптимизация компилятора. С какой оптимизацией компилятора вы компилируете? Макс?

Ted Lyngmo 04.08.2024 03:07

Как написано, я не нашел причин, по которым компилятор мог бы оставить какие-либо вычисления в конечной программе. Детали, которые, по вашему мнению, измерены, на самом деле могут быть просто удалены, поскольку результат не проверяется.

Ted Lyngmo 04.08.2024 03:15

Несвязано: if (a == b) return; — субоптимизация. В большинстве случаев вызывается swap, когда вы хотите произвести замену. Те несколько раз, когда ваша swap функция вызывается с указателем на одну и ту же int, просто поменяйте местами указанные int - ничего не произойдет. Тем не менее, вместо этого используйте std::swap.

Ted Lyngmo 04.08.2024 03:19

Код — C, ничего не показано на C++. #define TEST(func, begin, end){...выглядит ужасно. Кажется, вам нужно изучить указатели на функции.

3CxEZiVlQ 04.08.2024 03:24

Я отключил оптимизацию: $ gcc -O0 test.c, но результат: Тест Insertion_sort_1: 5317 мс; Тест Insertion_sort_2: 7825 мс.

user24402200 04.08.2024 03:42

Да, даже если эти два значения равны, операция замены выполняется без каких-либо побочных эффектов.

user24402200 04.08.2024 03:43

@user24402200 user24402200 Это было просто наблюдение. Это не решение. Не пишите такой код.

Ted Lyngmo 04.08.2024 04:10
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
7
75
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Два дефекта делают тест бессмысленным:

  1. В макросе TEST вы используете tmp - end + begin для вычисления указателя end массива tmp, но это должно быть tmp + (end - begin).

  2. insertion_sort_2() возвращает отсортированный массив, но он не содержит все элементы входного массива, поэтому это нечто иное, чем алгоритм сортировки. Например, вот прогон с N=10:

    before: 5298, 8764, 9261, 4325, 1305, 2502, 7606, 3141, 8770, 331
    after:  8764, 8764, 8764, 8764, 8764, 8764, 8770, 9261, 9261, 9261 
    

    Условие внутреннего цикла — i > begin && *(i - 1) > tmp, что означает, что в данном случае цикл завершается i == begin || *(i - 1) <= tmp. Оператор после цикла *(i-1) = tmp может и будет записываться перед массивом, что является неопределенным поведением. Должно быть *i = tmp;.

Как только вы исправите эти две проблемы, тест будет работать примерно за одно и то же время при компиляции с gcc -O2:

$ for((i=0; i < 3; i++)); do ./a.out ; done
Test insertion_sort_1 :         OK      831 ms
Test insertion_sort_2 :         OK      815 ms
Test insertion_sort_1 :         OK      947 ms
Test insertion_sort_2 :         OK      955 ms
Test insertion_sort_1 :         OK      831 ms
Test insertion_sort_2 :         OK      867 ms

За исключением меток, ассемблер этих двух функций один и тот же.

Я заменил ваш макрос TEST на функцию benchmark() для простоты отладки, добавив отсутствующий заголовок и несколько проверок ошибок:

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define N 100000

bool check(const int* begin, const int* end);

void benchmark(const char *name, void (*f)(int *, int *), int* begin,  int* end) {
    printf("Test %s : ", name);
    size_t n = end - begin;
    int *tmp = malloc(sizeof(int) * n);
    if (!tmp) {
        perror("");
        exit(1);
    }
    memcpy(tmp, begin, sizeof(int) * n);
    long long b = clock();
    (*f)(tmp, tmp + n);
    long long e = clock();
    if (check(tmp, tmp + n))
        printf("\tOK");
    else\
        printf("\tWrong");
    printf("\t%lld ms\n", (e - b) * 1000 / CLOCKS_PER_SEC);\
    free(tmp);
}

bool check(const int* begin, const int* end) {
    for(const int* cur = begin; cur < end - 1; ++cur) {
        if (*cur > *(cur + 1))
            return false;
    }
    return true;
}

int *geneArr(unsigned n) {
    int* arr = malloc(sizeof(int) * n);
    if (!arr) {
        perror("");
        exit(1);
    }
    for(int i = 0; i < n; ++i)
        arr[i] = rand() % 10000;
    return arr;
}

void swap(int* a, int* b) {
    if (a == b) return;
    int c = *a;
    *a = *b;
    *b = c;
}

void insertion_sort_1(int *begin, int *end) {
    for (int *cur = begin + 1; cur < end; cur++) {
        int tmp = *cur;
        int *pos = cur;
        for (int *i = cur; i > begin && *(i - 1) > tmp; --i) {
            *i = *(i - 1);
            pos = i - 1;
        }
        *pos = tmp;
    }
}

void insertion_sort_2(int *begin, int *end) {
    for (int *cur = begin + 1; cur < end; cur++) {
        int tmp = *cur;
        int *i = cur;
        for (; i > begin && *(i - 1) > tmp; --i)
            *i = *(i - 1);
        *i = tmp;
    }
}

int main() {
    srand(time(0));
    int* arr = geneArr(N);
    benchmark("insertion_sort_1", insertion_sort_1, arr, arr + N);
    benchmark("insertion_sort_2", insertion_sort_2, arr, arr + N);
    free(arr);
}

Другие вопросы по теме