Циклы for становятся медленнее, чем дольше они повторяются? Почему эти два исполнения цикла for различны?

В настоящее время я создаю собственную структуру BIG_INTEGER на чистом C в качестве проекта для времяпрепровождения. Одна из функций, которую я решил реализовать в своем проекте, — это PRNG, которая возвращает случайное BIG_INTEGER с указанным количеством цифр.

Это структура BIG_INTEGER:

typedef struct BIG_INTEGER
{
    BOOL bHasSign, bInit;
    SIZE_T szLength;
    INT* arrnDigits;
} BIG_INTEGER, *PBIG;

Это код, который я придумал для PRNG.

VOID birndl(PBIG pbigOut, ULONG szLength)
{
    if (pbigOut->bInit != 1)
    {
        // Initialize the input with binew() first.
        fprintf(stderr, "birndl!1: no content");
        abort();
    }
    else
        // Simply resets the values and frees the int array
        bidel(pbigOut);
    // Prepare
    pbigOut->bInit = TRUE;
    pbigOut->bHasSign = FALSE;
    pbigOut->szLength = szLength;
    pbigOut->arrnDigits = malloc(szLength * sizeof(INT));
    if (pbigOut->arrnDigits != NULL)
    {
        NTSTATUS ntErr;
        UCHAR buf[512];
        SIZE_T index1, index2, index3, index4;
        // Fill buffer with random bytes
        ntErr = BCryptGenRandom(NULL, buf, 512, BCRYPT_USE_SYSTEM_PREFERRED_RNG);
        if (!NT_SUCCESS(ntErr))
        {
            fprintf(stderr, "birndl!2: bcrypt failed with code %ld", ntErr);
            abort();
        }
        index1 = index2 = index3 = index4 = 0;
        *(pbigOut->arrnDigits) = (buf[index1] * buf[index2] * buf[index3] * buf[index4]) % 10;
        // Zero cannot be the first digit, since the resulting length would be szLength - 1
        // Slightly biased, but the show must go on; transfer the chances of getting a #0 to #5
        if (*(pbigOut->arrnDigits) == 0)
            *(pbigOut->arrnDigits) = 5;
        // By multiplying 4 selected UCHAR in the buffer mod 10, a digit from 0-9 is obtained
        for (SIZE_T _iter = 1; _iter < szLength; ++_iter)
        {
            // Select a different UCHAR from the buffer for every iteration
            if (index1 == 512 && index2 < 512)
            {
                index2++;
                index1 = 0;
            }
            else if (index1 == 512 && index2 == 512 && index3 < 512)
            {
                index3++;
                index2 = 0;
            }
            else if (index1 == 512 && index2 == 512 && index3 == 512 && index4 < 512)
            {
                index4++;
                index3 = 0;
            }
            else if (index1 == 512 && index2 == 512 && index3 == 512 && index4 == 512)
            {
                fprintf(stderr, "birndl!3: exceeding capability");
                abort();
            }
            else
                index1++;
            *(pbigOut->arrnDigits + _iter) = (buf[index1] * buf[index2] * buf[index3] * buf[index4]) % 10;
        }
    }
    else
    {
        fprintf(stderr, "birndl!4: memory error");
        abort();
    }
}

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

LARGE_INTEGER frequency, start, end;
QueryPerformanceFrequency(&frequency);
QueryPerformanceCounter(&start);
ULONGLONG length = 10000000;
BIG_INTEGER num;
binew(NULL, &num);
birndl(&num, length);
PCHAR str = bistr(&num);
bidel(&num);
free(str);
QueryPerformanceCounter(&end);
printf("QPC: Program took %fs to finish. (digits=%I64u)\n",
    (double)((end.QuadPart - start.QuadPart)) / frequency.QuadPart, length);
_CrtDumpMemoryLeaks();

Обратите внимание, что я пробовал это и с другими степенями 10 ниже, чем 10M. Это сводка его производительности с использованием того же метода устранения неполадок, за исключением того, что я соответствующим образом изменил значение length.

1-10 K digits: instantly
100 K digits: 0.5~0.6 seconds
1 M digits: 47~53 seconds
2 M digits: ~197 seconds
10 M digits: >10 minutes and still not finished

Если следовать здравому смыслу, кто генерирует 10M цифры, верно? Однако я хотел знать, есть ли лучший и быстрый способ генерировать такие большие суммы. Я подумал, может быть, если скорость for-цикла снижается по мере выполнения итерации, то, возможно, я смогу сначала разделить значения, передать их через birndl с меньшими итерациями, преобразовать эти BIG_INTEGER результаты в строку и, наконец, объединить их в одну большую строку. . Вот код точки входа вместе с bistr, который преобразует мой BIG_INTEGER в строку.

INT main(VOID)
{
    LARGE_INTEGER frequency, start, end;
    QueryPerformanceFrequency(&frequency);
    QueryPerformanceCounter(&start);
    // Compute necessary values
    ULONGLONG aim_digits = 10000000;
    UINT divisions = 2500;
    ULONGLONG per_division = aim_digits / divisions;
    // Create an array of BIG_INTEGER values
    BIG_INTEGER* list = malloc(sizeof(BIG_INTEGER) * divisions);
    // Store the string forms of the BIG_INTEGER values here
    // Last element (hence +1) would contain the concatenation of all strings in the array
    PCHAR* strlist = malloc(sizeof(PCHAR*) * (divisions + 1));
    assert(list != NULL && strlist != NULL);
    // Iterate for each division
    for (SIZE_T i = 0; i < divisions; ++i)
    {
        binew(NULL, &list[i]);
        birndl(&list[i], per_division);
        // Convert generated portion into a string
        strlist[i] = bistr(&list[i]);
    }
    // Allocate
    strlist[divisions] = malloc(aim_digits + 1);
    assert(strlist[divisions] != NULL);
    // Start concatenating everything
    strcpy(strlist[divisions], strlist[0]);
    for (SIZE_T i = 1; i < divisions; ++i)
        strcat(strlist[divisions], strlist[i]);
    // Make sure to free all the used memory by the end
    for (SIZE_T i = 0; i < divisions; ++i)
    {
        bidel(&list[i]);
        free(strlist[i]);
    }
    free(strlist[divisions]);
    free(strlist);
    free(list);
    QueryPerformanceCounter(&end);
    printf("QPC: Program took %fs to finish. (digits=%I64u)\n",
        (double)((end.QuadPart - start.QuadPart)) / frequency.QuadPart, aim_digits);
    _CrtDumpMemoryLeaks();
    return 0;
}

PCHAR bistr(PBIG pbigIn)
{
    if (pbigIn->bInit != 1)
    {
        fprintf(stderr, "bistr!1: no content");
        abort();
    }
    PCHAR pstrOut = calloc(pbigIn->szLength + pbigIn->bHasSign + 1, sizeof(CHAR));
    if (!pstrOut)
    {
        fprintf(stderr, "bistr!2: memory error");
        abort();
    }
    if (pbigIn->bHasSign)
        strcat(pstrOut, "-");
    for (SIZE_T i = 0; i < pbigIn->szLength; ++i)
    {
        CHAR digit[2];
        snprintf(digit, 2, "%d", pbigIn->arrnDigits[i]);
        strcat(pstrOut, digit);
    }
    return pstrOut;
}

Я провел тестовый прогон «логарифмически» с примерно 4000 цифрами на деление, и это дало чрезвычайно потрясающие результаты!

d - digits; v - divisions
10d,5v:        }
100d,25v:      }
1Kd,25v:       } instantly
10Kd,25v:      }
100Kd,25v:     }
1Md,250v: 0.5~0.6 seconds
10Md,2500v: 6.6~7.0 seconds
100Md,25000v: ~197 seconds

Это определенно намного быстрее, чем простой способ использования PRNG. Он генерирует 100 миллионов цифр за то же время, что и первый метод, когда он пытался сгенерировать 2 миллиона!

Однако, как следует из названия, мне также интересно, почему birndl приводит к тому, что скорость for-цикла со временем ухудшается. Есть ли способ оптимизировать его так, чтобы он работал так же или, по крайней мере, почти так же, как «метод разделения», или это просто природа for-циклов замедляться со временем во время итераций, что следует сохранять количество итераций мало для поддержания эффективности?

Почему второй метод будет быстрее, который использует birndl много раз среди других дополнительных шагов (bistr, strcat), но с меньшим количеством пройденных итераций по сравнению с первым методом, который просто говорит birndl сгенерировать очень большое количество цифр за один раз?

Спасибо!

«или это просто природа циклов for замедляться с течением времени» нет, это не природа циклов for замедляться с течением времени.

matt 30.08.2024 21:06

@matt Хорошо, тогда на скорость должно влиять то, что внутри него, и единственная строка, которую можно заподозрить, - это присвоение элементу массива int, не так ли?

amegyoushi 30.08.2024 21:20

Ваша функция кажется излишним. Генератор случайных чисел выдаст вам случайную последовательность. Нет необходимости массировать его. Просто назначьте n бибитов каждому при вызове ГСЧ.

Dúthomhas 30.08.2024 21:30

Кроме того, нет необходимости использовать загадочные имена. bi_generate_random или bi_random_n или что-то в этом роде гораздо более читабельно, чем birndl.

Dúthomhas 30.08.2024 21:30

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

amegyoushi 30.08.2024 22:00

Я пытался имитировать имена функций Ассамблеи и Excel, ха-ха-ха, я ценю замечание!

amegyoushi 30.08.2024 22:03

@amegyoushi, ты сделал ГСЧ менее безопасным, потому что он будет выдавать больше нулевых цифр, чем 9 цифр. Надеюсь, вы поймете, почему, прежде чем пытаться что-либо сделать в области шифрования.

Phil Willoughby 30.08.2024 22:26

В общем, доверяйте своему ГСЧ. Почти все, что вы с ним делаете, добавляет предвзятости, обычно в довольно заметных закономерностях.

Dúthomhas 31.08.2024 05:01

Попытка сделать ГСЧ «более случайным» с помощью массажа — классическая ошибка.

Mitch Wheat 31.08.2024 05:14
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
9
110
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

Циклы for не замедляются со временем.

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

Еще несколько советов:

  1. Попробуйте использовать полбайта для каждой десятичной цифры вместо целого числа. Вы обнаружите, что это требует меньше памяти и работает быстрее.
  2. Ваш ГПСЧ не даст равномерно распределенных результатов. Пример более справедливого и быстрого алгоритма может взять 4 случайных бита из вашего буфера, использовать их в качестве цифрового значения, если оно < 10, и отбросить его в противном случае. Кроме того, при использовании любого алгоритма вам следует снова вызвать BCryptGenRandom, когда вы исчерпаете буфер.

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

Dúthomhas 30.08.2024 21:35

Спасибо за дополнительные советы! Я буду иметь в виду изменить массив INT на массив SHORT. Однако мне до сих пор не ясно, почему второй метод будет быстрее, учитывая, что он будет выделять больше памяти по сравнению с простым. Пожалуйста, потерпите меня, потому что я все еще не эксперт в этом. Массив BIG_INTEGER обойдется мне в 24 байта за каждый элемент, а затем массив строк, в котором будут храниться строковые эквиваленты, обойдется мне в 8 байт за каждый элемент. Разве они не должны иметь одинаковую скорость, поскольку используют один и тот же объем памяти, за исключением того, что у второго метода есть дополнительная?

amegyoushi 30.08.2024 21:46

@amegyoushi, почему ты выделяешь строковые эквиваленты? Предположим, существует последовательность вычислений bigint. Зачем вам нужны строковые эквиваленты, кроме специальных, когда вам нужно читаемое значение? Мои собственные массивы bigint имеют 32-битные значения и 64-битные вычисления. В некоторых случаях я использую систему счисления 10^9, когда требуется более простое преобразование между десятичными строками.

Weather Vane 30.08.2024 22:00
short будет 2 байта или 16 бит на десятичную цифру. Я предлагаю вам упаковать пары десятичных цифр в unsigned char, что составляет 4 бита на десятичную цифру.
Phil Willoughby 30.08.2024 22:10

@WeatherVane Когда я читаю код, единственное хранилище для большого целого числа находится в выделенной последовательности целых чисел, и каждое из них находится в диапазоне 0–9 (за исключением того, что первое находится в диапазоне 1–9).

Phil Willoughby 30.08.2024 22:13

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

amegyoushi 30.08.2024 22:17

@PhilWilloughby О, ладно, я понимаю, что ты имеешь в виду! Извините за это.

amegyoushi 30.08.2024 22:19

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