Являются ли операторы goto неизбежными в этом C-коде? - PullRequest
0 голосов
/ 10 января 2019

Я не могу написать свой код без использования операторов goto. Они неизбежны?

Я написал такой код:

sequence - это функция, которая печатает все последовательности, состоящие из m чисел от 0 до (n - 1), и возвращает их количество.

Это работает, как я и ожидал, но я использовал три метки и три оператора goto.

Я также написал sequence_substitute , не используя операторов goto, но он медленнее, чем sequence , когда n> 9.

//this prints all sequences that consist of m numbers from 0 to (n - 1), and return the number of them.
//for example, if m = 2 and n = 3 then it prints "0 0 / 0 1 / 0 2 / 1 0 / 1 1 / 1 2 / 2 0 / 2 1 / 2 2 / " and return 9.
int sequence(int m, int n){
    int i[100], j = 0, count = 0;
A:
    if(!(j < m)){
        for(int k = 0; k < m; ++k) printf("%d ", i[k]);
        printf("/ ");
        ++count;
        goto C;
    }
    i[j] = 0;
B:
    if(i[j] < n){
        ++j;
        goto A;
    }
C:
    --j;
    if(j >= 0){
        ++i[j];
        goto B;
    }
    putchar('\n');
    return count;
}

int sequence_substitute(int m, int n){
    int i[100], count = 0;
    for(int j = 0; j < m; ++j) i[j] = 0;
    for(;;){
        int j = m - 1;
        for(int k = 0; k < m; ++k) printf("%d ", i[k]);
        printf("/ ");
        ++count;
        for(;;){
            if(i[j] < n - 1){
                ++i[j];
                break;
            }else{
                if(j == 0){
                    putchar('\n');
                    return count;
                }else{
                    i[j] = 0;
                    --j;
                }
            }
        }
    }
}

Есть ли способы написать функцию так быстро, как sequence без использования операторов goto?

Ответы [ 4 ]

0 голосов
/ 10 января 2019

Сначала подсчитаем количество результатов. Это будет numbers_of_results = pow(n, m). Например. если m = 2 и n = 3, то будут pow(3, 2) == 9 результаты.

Как только вы узнаете, сколько существует результатов, вы можете использовать это для индексации пространства решения. Например:

    numbers_of_results = pow(n, m);
    for(index = 0; index < numbers_of_results; index++) {

    }

Индекс - это просто «базовое число m», где каждая цифра в индексе определяет число. Чтобы извлечь отдельные цифры из индекса, вы можете использовать digit = (index / pow(m, digit_number) ) % m, но этого pow() можно избежать, когда вы извлекаете все цифры по одной за раз.

Например:

unsigned int sequence(unsigned int m, unsigned int n) {
    unsigned int numbers_of_results, index, temp, digit_number;

    numbers_of_results = pow(n, m);
    for(index = 0; index < numbers_of_results; index++) {
        temp = index;
        for(digit_number = 0; digit_number < n; digit_number++) {
           digit = temp % m;
           temp /= m;
           printf("%d ", digit);
        }
        printf("/ ");
    }
    putchar('\n');
    return numbers_of_results;

Теперь; подумайте о компромиссе «удобочитаемость / ремонтопригодность и производительность» и (если вы заботитесь о производительности вообще), какие ЦП вам интересны.

Для некоторых процессоров (и не для других) может быть лучше заменить pow() вашей собственной реализацией (предназначенной только для целых чисел). Для некоторых процессоров (а не для других) было бы полезно добавить версии для особых случаев (например, для всех случаев, когда m - это степень 2, а деления и по модулю можно заменить на «сдвиг на константу m» и «И»). с постоянной m-1 "). Для некоторых процессоров (и не для других) деление и модуль могут быть намного дороже, чем затраты на ошибочное прогнозирование веток, поэтому это может повысить производительность до округления m до ближайшей степени 2 (так что вы можете использовать одно из «сдвигов» и AND "специальные версии), но затем исключить / исключить случаи, когда индекс содержит слишком большую цифру.

0 голосов
/ 10 января 2019

То, на чем вы сосредоточены, не имеет никакого отношения к реальному миру. Нет, я не упрекаю тебя; Я только указываю, что вы беспокоитесь о чем-то, что не имеет значения.

Самая эффективная реализация - та, которая не работает.

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

Достаточно хорошо, легко модифицировать / обновлять, стоит гораздо больше.

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

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

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

Тогда проблема заключается в разнице между бенчмаркингом и бенчмаркингом .

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

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

Распространенной проблемой является масштабируемость . Некоторые функции выполняются быстрее, чем другие, когда данных много, но когда набор данных невелик, обратное может быть истинным. Это особенно характерно для функций сортировки. Некоторые функции сортировки очень медленные, если данных не так много; но быстрее, чем любая другая функция, когда у вас есть достаточно данных. (Радикальная сортировка для числовых данных является прекрасным примером: она сортирует по существу линейное время, но линейный коэффициент настолько велик, что вам нужны массивы с миллионами записей, прежде чем радикальная сортировка займет меньше времени, чем другие алгоритмы сортировки; радикальная сортировка также имеет тенденцию к быть более загруженным кешем, чем другие алгоритмы, поэтому, хотя в определенных терминах он «превосходит» его, он не часто используется.)

Мы используем термин микробенчмарк при сравнении, например. работает с синтетическими тестовыми примерами, чтобы напомнить нам людям, что такие тесты показательны и не совсем надежны по вышеуказанным причинам.

Микробенчмаркинг сам по себе - искусство делать правильно.

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

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

По этим причинам я рекомендую использовать медиану или какой-то другой процентиль вместо среднего.

Если вы думаете об этом, вас редко интересует, сколько, скажем, может потребоваться 1000 последовательных вызовов, потому что вы не выполняете эти вызовы последовательно в реальных программах. Однако, если вы знаете, что среднее время каждого вызова функции составляет T , вы знаете, что, по крайней мере, в 50% случаев с микробенчмарками, вызов функции занимал T или меньше времени.

Существует также проблема , какие часы использовать на компьютере. Мы можем измерить время настенных часов (clock_gettime(CLOCK_MONOTONIC, &timespec) в системах POSIXy) или только время ЦП (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &timespec)), занятое этим конкретным потоком. Время настенных часов зависит от всего, что работает на той же машине, но лучше соответствует тому, что человек может ожидать от производительности; процессорное время лучше для математических вычислений, которые не требуют значительных объемов данных. (Я бы сказал, что процессорное время лучше для таких функций, как OP.)

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

(Конечно, все результаты специфичны для каждой машины и различаются в зависимости от компилятора, опций компилятора и используемых библиотек. Тот факт, что микробенчмарк X быстрее на машине А, чем на машине Б, не означает, что микробенчмарк Y также быстрее на A, чем на B. Помните: ориентировочный, а не доказательный или действительно надежный.)

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

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

void sequence(int m, int n, void (*callback)(int index, char seq[]))
{
    int   index[m + 1];
    char  buffer[m + 1];
    int   count = 0;
    int   j = 0;

    buffer[m] = '\0';

A:
    if (j >= m) {
        callback(count, buffer);
        ++count;
        goto C;
    }
    index[j] = 0;

B:
    if (index[j] < n) {
        ++j;
        goto A;
    }

C:
    --j;
    if (j >= 0) {
        ++index[j];
        goto B;
    }

    return;
}

Я также переименовал массив i в index. Описательные имена работают лучше. Допустим, мы поместили это в fiveseven.c . Давайте также напишем небольшой Makefile , чтобы помочь нам скомпилировать этот материал:

CC      := gcc
CFLAGS  := -Wall -O2
LDFLAGS :=
LIBS    := fiveseven.so

.PHONY: all clean

all: clean benchmark $(LIBS)

clean:
        rm -f benchmark $(LIBS)

%.so: %.c
        $(CC) $(CFLAGS) -fPIC -shared $^ -Wl,-soname,$@ $(LDFLAGS) -o $@

benchmark: benchmark.c
        $(CC) $(CFLAGS) $^ $(LDFLAGS) -ldl -o $@

Обратите внимание, что для отступа используются Tab s, а не пробелы; но этот форум не позволяет тем, кто вставил код. Поэтому, если вы скопируете и вставите вышеперечисленное, запустите sed -e 's|^ *|\t|' -i Makefile, чтобы исправить отступ.

Компилирует реальную программу микробенчмаркинга, от benchmark.c до ./benchmark, отдельно; и функцию или функции, которые нужно сравнить, в динамически загружаемые библиотеки. Это позволяет избежать удивительной оптимизации компилятором кода.

Если вы добавляете другие реализации, просто добавьте имя их библиотеки (заканчивающееся .so в Linux) в строку LIBS. Обратите внимание, что вы также можете запустить make CC=clang или make LIBS=foo.so clean all и т. Д., Переопределяя переменные во время сборки.

Конечно, нам нужна сама программа бенчмаркинга. Вот одна реализация, benchmark.c:

#define _POSIX_C_SOURCE  200809L
// SPDX-License-Identifier: CC0-1.0

#include <stdlib.h>
#include <inttypes.h>
#include <dlfcn.h>
#include <stdio.h>
#include <time.h>

typedef void (sequence_fn)(int, int, void (*)(int, char []));

static int  compare_int64(const void *ptr1, const void *ptr2)
{
    const int64_t  val1 = *(const uint64_t *)ptr1;
    const int64_t  val2 = *(const uint64_t *)ptr2;
    return (val1 < val2) ? -1 :
           (val1 > val2) ? +1 : 0;
}

static void  nothing(int index, char value[])
{
    return;
}

static double  median_cpu_time(const size_t  iterations,
                               const int     length,
                               const int     radix,
                               sequence_fn  *sequence_function)
{
    struct timespec  started, stopped;
    int64_t         *nanosecs;
    double           result;
    size_t           i;

    if (iterations < 1 || length < 1 || radix < 1 || !sequence_function)
        return -1.0; /* Invalid parameters. */

    nanosecs = malloc(iterations * sizeof nanosecs[0]);
    if (!nanosecs)
        return -1.0; /* Out of memory. */

    for (i = 0; i < iterations; i++) {

        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &started);
        sequence_function(length, radix, nothing);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &stopped);

        nanosecs[i] = (int64_t)(stopped.tv_sec - started.tv_sec) * INT64_C(1000000000)
                    + (int64_t)(stopped.tv_nsec - started.tv_nsec);
    }

    qsort(nanosecs, iterations, sizeof (int64_t), compare_int64);
    result = (double)nanosecs[iterations / 2] / 1000000000.0;
    free(nanosecs);
    return result;
}

int main(int argc, char *argv[])
{
    size_t       iterations;
    int          arg, length, radix;
    void        *handle;
    sequence_fn *func;    
    double       seconds;
    char         dummy;

    if (argc < 5) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help | help ]\n", argv[0]);
        fprintf(stderr, "       %s LENGTH RADIX ITERATIONS ./library.so ...\n", argv[0]);
        fprintf(stderr, "\n");
        fprintf(stderr, "This program measures the median CPU time taken by\n");
        fprintf(stderr, "each sequence(LENGTH, RADIX, callback) call\n");
        fprintf(stderr, "over ITERATIONS iterations, as implemented in each\n");
        fprintf(stderr, "listed dynamic library.\n");
        fprintf(stderr, "\n");
        return EXIT_FAILURE;
    }

    if (sscanf(argv[1], " %d %c", &length, &dummy) != 1 || length < 1) {
        fprintf(stderr, "%s: Invalid length.\n", argv[1]);
        return EXIT_FAILURE;
    }

    if (sscanf(argv[2], " %d %c", &radix, &dummy) != 1 || radix < 1) {
        fprintf(stderr, "%s: Invalid radix.\n", argv[2]);
        return EXIT_FAILURE;
    }

    if (sscanf(argv[3], " %zu %c", &iterations, &dummy) != 1 || iterations < 1) {
        fprintf(stderr, "%s: Invalid number of iterations.\n", argv[3]);
        return EXIT_FAILURE;
    }

    printf("Reporting median CPU time used over %zu iterations.\n", iterations);
    printf("Length = %d, radix = %d.\n", length, radix);
    fflush(stdout);
    for (arg = 4; arg < argc; arg++) {

        handle = dlopen(argv[arg], RTLD_NOW);
        if (!handle) {
            fprintf(stderr, "%s: %s.\n", argv[arg], dlerror());
            continue;
        }

        func = dlsym(handle, "sequence");
        if (!func) {
            fprintf(stderr, "%s: Library does not implement sequence().\n", argv[arg]);
            dlclose(handle);
            continue;
        }

        printf("    %s: ", argv[arg]);
        fflush(stdout);

        seconds = median_cpu_time(iterations, length, radix, func);

        printf("%.9f seconds median per call\n", seconds);
        fflush(stdout);

        dlclose(handle);
    } 

    return EXIT_SUCCESS;
}

Обратите внимание, что он предоставляет функцию обратного вызова, которая ничего не делает, и что большая часть программы на самом деле просто предоставляет информацию об использовании командной строки. Это удивительно полезно на практике. Я склонен помещать каждый «проект» в отдельный каталог / папку. Когда я ищу конкретную вещь, я делаю find + grep или grep -e pattern -R base-of-directories, а затем проверяю вероятных кандидатов. Если есть исполняемый файл, который я могу запустить, чтобы увидеть использование (мое всегда есть!), Я могу увидеть цель этого конкретного каталога; и гораздо быстрее и менее утомительно читать тысячи строк кода, чтобы попытаться вспомнить, если это то, что я искал.

После сохранения вышеприведенного, запустите, например,

make clean all
./benchmark 5 4 100000 ./fiveseven.so

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

Обратите внимание, что этот микробенчмарк ничего не делает для проверки правильности результатов. Это то, что следует учитывать.

0 голосов
/ 10 января 2019

Скорее, как ючики в их ответе , я сравнил код. Я также придумал собственное решение проблемы.

Я выполнил свои тесты на своем MacBook Pro (15 дюймов, 2017 г.) с процессором Intel Core i7 с частотой 2,9 ГГц, работающим на MacOS 10.14.2 Mojave и использующим самодельный GCC 8.2.0, а также временной код, доступный в моем * Репозиторий 1007 * SOQ (вопросы переполнения стека) на GitHub в виде файлов timer.c и timer.h в подкаталоге src / libsoq . Я переименовал функцию sequence из вопроса в sequence_withgoto, чтобы дать ее имени аналогичную длину другим функциям. Я удалил (в результате комментирования) код печати в функциях генератора последовательности. Я изменил тип счетчика с int на unsigned (хотя можно утверждать, что он может / должен быть unsigned long long, чтобы увеличить диапазон). Максимальный тест, показанный ниже, точно переполняет 32-битный тип unsigned, давая ответ 0.

Тестовый код без комментариев (исходный файл seq23.c):

#include <assert.h>
#include <stdio.h>
#include "timer.h"

static unsigned sequence_withgoto(int m, int n)
{
    int i[100], j = 0;
    unsigned count = 0;
A:
    if (!(j < m))
    {

        ++count;
        goto C;
    }
    i[j] = 0;
B:
    if (i[j] < n)
    {
        ++j;
        goto A;
    }
C:
    --j;
    if (j >= 0)
    {
        ++i[j];
        goto B;
    }

    return count;
}

static unsigned sequence_substitute(int m, int n)
{
    int i[100];
    unsigned count = 0;
    for (int j = 0; j < m; ++j)
        i[j] = 0;
    for ( ; ; )
    {
        int j = m - 1;

        ++count;
        for ( ; ; )
        {
            if (i[j] < n - 1)
            {
                ++i[j];
                break;
            }
            else
            {
                if (j == 0)
                {

                    return count;
                }
                else
                {
                    i[j] = 0;
                    --j;
                }
            }
        }
    }
}

static unsigned generate_sequence(int m, int n)
{
    assert(m <= n);
    assert(m > 0);
    assert(n < 20);

    int data[m];
    for (int i = 0; i < m; i++)
        data[i] = 0;

    unsigned counter = 0;
    while (data[0] < n)
    {

        counter++;
        for (int i = m - 1; i >= 0; i--)
        {
            if (++data[i] < n)
                break;
            if (i == 0)
                break;
            data[i] = 0;
        }
    }

    return counter;
}

static void time_sequence_generator(const char *tag, int m, int n, unsigned (*function)(int m, int n))
{
    Clock clk;

    clk_init(&clk);
    clk_start(&clk);
    unsigned count = (*function)(m, n);
    clk_stop(&clk);
    char buffer[32];
    printf("Number of sequences (m = %d, n = %d): %u elapsed = %s (%s)\n",
           m, n, count, clk_elapsed_us(&clk, buffer, sizeof(buffer)), tag);
}

static void test_sequence_generators(int m, int n)
{
    time_sequence_generator("generate_sequence", m, n, generate_sequence);
    time_sequence_generator("sequence_withgoto", m, n, sequence_withgoto);
    time_sequence_generator("sequence_substitute", m, n, sequence_substitute);
}

int main(void)
{
    test_sequence_generators(2, 3);
    test_sequence_generators(5, 9);
    test_sequence_generators(4, 10);
    test_sequence_generators(6, 15);
    test_sequence_generators(7, 19);
    test_sequence_generators(8, 16);

    return 0;
}

Командная строка компиляции:

gcc -O3 -g -I./inc -std=c11 -Wall -Wextra -Werror  seq23.c -o seq23 -L./lib -lsoq 

Заголовки установлены в ./inc, а библиотека, содержащая код таймера, находится в ./lib (в статической библиотеке libsoq.a).

Результаты, которые я получил, поразительны и последовательны за несколько прогонов:

Number of sequences (m = 2, n = 3): 9 elapsed = 0.000005 (generate_sequence)
Number of sequences (m = 2, n = 3): 9 elapsed = 0.000000 (sequence_withgoto)
Number of sequences (m = 2, n = 3): 9 elapsed = 0.000000 (sequence_substitute)
Number of sequences (m = 5, n = 9): 59049 elapsed = 0.000098 (generate_sequence)
Number of sequences (m = 5, n = 9): 59049 elapsed = 0.000119 (sequence_withgoto)
Number of sequences (m = 5, n = 9): 59049 elapsed = 0.000068 (sequence_substitute)
Number of sequences (m = 4, n = 10): 10000 elapsed = 0.000012 (generate_sequence)
Number of sequences (m = 4, n = 10): 10000 elapsed = 0.000015 (sequence_withgoto)
Number of sequences (m = 4, n = 10): 10000 elapsed = 0.000010 (sequence_substitute)
Number of sequences (m = 6, n = 15): 11390625 elapsed = 0.013260 (generate_sequence)
Number of sequences (m = 6, n = 15): 11390625 elapsed = 0.015959 (sequence_withgoto)
Number of sequences (m = 6, n = 15): 11390625 elapsed = 0.010123 (sequence_substitute)
Number of sequences (m = 7, n = 19): 893871739 elapsed = 1.064473 (generate_sequence)
Number of sequences (m = 7, n = 19): 893871739 elapsed = 1.206680 (sequence_withgoto)
Number of sequences (m = 7, n = 19): 893871739 elapsed = 0.758287 (sequence_substitute)
Number of sequences (m = 8, n = 16): 0 elapsed = 4.819932 (generate_sequence)
Number of sequences (m = 8, n = 16): 0 elapsed = 5.712081 (sequence_withgoto)
Number of sequences (m = 8, n = 16): 0 elapsed = 3.705033 (sequence_substitute)

Последовательность (m = 8, n = 16) генерирует 16 8 = 2 32 последовательностей, что означает, что счетчик unsigned переполняется до 0.

Что меня поразило, так это то, что sequence_withgoto() - самая медленная из функций; мой generate_sequence() стоит посередине, за исключением самых маленьких проблем; и sequence_substitute() из вопроса - самый быстрый, с заметным запасом (примерно в 2/3 времени варианта, использующего goto в последней последовательности).

Алгоритм, который я реализовал, описан в файле с комментариями:

/*
** Algorithm:
** Array data contains m values.
** The farthest right entry varies continuously; when it exceeds n-1, it
** is reset to 0.  If the increment wraps back to 0, then the previous
** index is incremented (and if it wraps to zero, ...).  The loop
** finishes when the zeroth index reaches n (wrapping is suppressed).
*/

Резюме

Чтобы ответить на главный вопрос в заголовке:

  • Нет; goto заявления не являются обязательными.

Чтобы ответить на вопрос о скорости в теле:

  • Код, использующий goto, обычно не работает быстрее при решении проблем большого размера (и, если есть небольшие преимущества в производительности при небольших проблемах, вряд ли этого будет достаточно).
0 голосов
/ 10 января 2019

Я сравнил эти две функции в следующем коде.

В этом тесте (m, n) = (6,15);

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

    double get_current_time();

    int sequence(int m, int n);
    int sequence_substitute(int m, int n);

    double benchmark(int (*method)(int, int), int m, int n) {
        double time1 = get_current_time();
        method(m, n);
        method(m, n);
        method(m, n);
        method(m, n);
        method(m, n);
        method(m, n);
        method(m, n);
        method(m, n);
        method(m, n);
        method(m, n);
        double time2 = get_current_time();
        return (time2 - time1) / 10;
    }

    int main(void) {
        const int m = 6;
        const int n = 15;
        fprintf(stderr, "sequence: %f\n", benchmark(sequence, m, n));
        fprintf(stderr, "sequence_substitute: %f\n",
                benchmark(sequence_substitute, m, n));
        return 0;
    }

    #if defined(WIN32) || defined(__WIN32) || defined(_WIN32) || \
        defined(__WIN32__) || defined(_WIN32_)
    #include <windows.h>

    double get_current_time() {
        LARGE_INTENGER t, f;
        QueryPerformanceCounter(&t);
        QueryPerformanceFrequency(&f);
        return (double)t.QuadPart / (double)f.QuadPart;
    }

    #else

    #include <sys/resource.h>
    #include <sys/time.h>

    double get_current_time() {
        struct timeval t;
        gettimeofday(&t, 0);
        return t.tv_sec + t.tv_usec * 1e-6;
    }

    #endif

    /**************************************************************************/

    // this prints all sequences that consist of m numbers from 0 to (n - 1), and
    // return the number of them. for example, if m = 2 and n = 3 then it prints "0
    // 0 / 0 1 / 0 2 / 1 0 / 1 1 / 1 2 / 2 0 / 2 1 / 2 2 / " and return 9.
    int sequence(int m, int n) {
        int i[100], j = 0, count = 0;
    A:
        if (!(j < m)) {
            for (int k = 0; k < m; ++k) printf("%d ", i[k]);
            printf("/ ");
            ++count;
            goto C;
        }
        i[j] = 0;
    B:
        if (i[j] < n) {
            ++j;
            goto A;
        }
    C:
        --j;
        if (j >= 0) {
            ++i[j];
            goto B;
        }
        putchar('\n');
        return count;
    }

    int sequence_substitute(int m, int n) {
        int i[100], count = 0;
        for (int j = 0; j < m; ++j) i[j] = 0;
        for (;;) {
            int j = m - 1;
            for (int k = 0; k < m; ++k) printf("%d ", i[k]);
            printf("/ ");
            ++count;
            for (;;) {
                if (i[j] < n - 1) {
                    ++i[j];
                    break;
                } else {
                    if (j == 0) {
                        putchar('\n');
                        return count;
                    } else {
                        i[j] = 0;
                        --j;
                    }
                }
            }
        }
    }

https://gist.github.com/yuchiki/26fd96a2791f7f6d2d9929b404a16da6

и результат будет следующим:

при компиляции с -O3,

  • последовательность: 5,390164 [сек]
  • sequence_substitute: 5.381983 [сек]

и при компиляции с -O0

  • последовательность: 5,178518 [с]
  • sequence_substitute: 5.256273 [сек]

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

Может быть, код, который вы здесь показываете, слишком расплывчат, чтобы воспроизвести разницу в скорости, о которой вы сообщили.

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

  • полный и точный код, включая main , любые прагмы или директивы, а также эталонный код
  • результат теста

Я попробовал другой бенчмарк для версии этих двух функций без ввода-вывода, используя следующий тестовый код:


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

    double get_current_time();

    int sequence(int m, int n);
    int sequence_substitute(int m, int n);

    double benchmark(int (*method)(int, int), int m, int n) {
        double time1 = get_current_time();
        method(m, n);
        method(m, n);
        method(m, n);
        method(m, n);
        method(m, n);
        method(m, n);
        method(m, n);
        method(m, n);
        method(m, n);
        method(m, n);
        double time2 = get_current_time();
        return (time2 - time1) / 10;
    }

    int main(void) {
        const int m = 7;
        const int n = 15;
        fprintf(stderr, "sequence: %f\n", benchmark(sequence, m, n));
        fprintf(stderr, "sequence_substitute: %f\n",
                benchmark(sequence_substitute, m, n));
        return 0;
    }

    #if defined(WIN32) || defined(__WIN32) || defined(_WIN32) || \
        defined(__WIN32__) || defined(_WIN32_)
    #include <windows.h>

    double get_current_time() {
        LARGE_INTENGER t, f;
        QueryPerformanceCounter(&t);
        QueryPerformanceFrequency(&f);
        return (double)t.QuadPart / (double)f.QuadPart;
    }

    #else

    #include <sys/resource.h>
    #include <sys/time.h>

    double get_current_time() {
        struct timeval t;
        gettimeofday(&t, 0);
        return t.tv_sec + t.tv_usec * 1e-6;
    }

    #endif

    /**************************************************************************/

    // this prints all sequences that consist of m numbers from 0 to (n - 1), and
    // return the number of them. for example, if m = 2 and n = 3 then it prints "0
    // 0 / 0 1 / 0 2 / 1 0 / 1 1 / 1 2 / 2 0 / 2 1 / 2 2 / " and return 9.
    int sequence(int m, int n) {
        int i[100], j = 0, count = 0;
    A:
        if (!(j < m)) {
            for (int k = 0; k < m; ++k) {
            }  // printf("%d ", i[k]);
            // printf("/ ");
            ++count;
            goto C;
        }
        i[j] = 0;
    B:
        if (i[j] < n) {
            ++j;
            goto A;
        }
    C:
        --j;
        if (j >= 0) {
            ++i[j];
            goto B;
        }
        // putchar('\n');
        return count;
    }

    int sequence_substitute(int m, int n) {
        int i[100], count = 0;
        for (int j = 0; j < m; ++j) i[j] = 0;
        for (;;) {
            int j = m - 1;
            for (int k = 0; k < m; ++k) {
            }  // printf("%d ", i[k]);
            // printf("/ ");
            ++count;
            for (;;) {
                if (i[j] < n - 1) {
                    ++i[j];
                    break;
                } else {
                    if (j == 0) {
                        // putchar('\n');
                        return count;
                    } else {
                        i[j] = 0;
                        --j;
                    }
                }
            }
        }
    }

результат выглядит следующим образом:

при компиляции с -O3,

  • последовательность: 0,019198 [сек]
  • sequence_substitute: 0,016234 [сек]

при компиляции с -O0,

последовательность: 0,136406 [сек] sequence_substitute: 0.112287 [сек]

Я не думаю, что результат версии -O3 имеет не так много смысла, поскольку можно предположить, что в этом случае оптимизатор удаляет большую часть кода. Но версия -O0 предполагала следующие факты:

  • Самая тяжелая часть этого кода - IO.
  • Логическая часть sequence_substitute также работает так же быстро, как и логическая часть sequence .
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...