Почему первые несколько назначений строк выполняются медленнее?

#c #string #performance #initialization

#c #строка #Производительность #инициализация

Вопрос:

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

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

#define START_COUNTING  clock_gettime(CLOCK_PROCESS_CPUTIME_ID, amp;start)
#define END_COUNTING    clock_gettime(CLOCK_PROCESS_CPUTIME_ID, amp;end)
#define NUM_OF_LOOPS    8192

long time_difference(struct timespec end, struct timespec beginning) {
    long long diff = (long long)end.tv_sec - (long long)beginning.tv_sec;
    return(end.tv_nsec - beginning.tv_nsec   ((diff > 0) ? 1000000L : 0));
}

long average(long numbers[NUM_OF_LOOPS]) {
    long long sum = 0;
    int i;

    for(i = 0; i < NUM_OF_LOOPS; i  )
        sum  = numbers[i];

    return sum / NUM_OF_LOOPS;
}

int main(void) {
    struct timespec start;
    struct timespec end;
    long time_diffs[NUM_OF_LOOPS];
    int i;
    char * str = NULL;

    for(i = 0; i < NUM_OF_LOOPS; i  ) {
        START_COUNTING;
        str = "T";
        END_COUNTING;
        time_diffs[i] = time_difference(end, start);
    }
    printf("%li ns - assigning 2 chars to a stringn", average(time_diffs));

    for(i = 0; i < NUM_OF_LOOPS; i  ) {
        START_COUNTING;
        str = "Testing";
        END_COUNTING;
        time_diffs[i] = time_difference(end, start);
    }
    printf("%li ns - assigning 8 chars to a stringn", average(time_diffs));

    for(i = 0; i < NUM_OF_LOOPS; i  ) {
        START_COUNTING;
        str = "Testing it here";
        END_COUNTING;
        time_diffs[i] = time_difference(end, start);
    }
    printf("%li ns - assigning 16 chars to a stringn", average(time_diffs));

    for(i = 0; i < NUM_OF_LOOPS; i  ) {
        START_COUNTING;
        str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit nullam.";
        END_COUNTING;
        time_diffs[i] = time_difference(end, start);
    }
    printf("%li ns - assigning 64 chars to a stringn", average(time_diffs));

    for(i = 0; i < NUM_OF_LOOPS; i  ) {
        START_COUNTING;
        str = ""; // String omitted because it would waste a lot of space
        END_COUNTING;
        time_diffs[i] = time_difference(end, start);
    }
    printf("%li ns - assigning 1024 chars to a stringn", average(time_diffs));

    return 0;
}
  

при профилировании я всегда получаю этот результат:

 1126 ns - assigning 2 chars to a string
828 ns - assigning 8 chars to a string
832 ns - assigning 16 chars to a string
834 ns - assigning 64 chars to a string
857 ns - assigning 1024 chars to a string
  

Первый результат, независимо от того, присваивается ли он 2 символам, 8 символам или сколько угодно, всегда значительно медленнее, чем остальные. Я попытался поместить 1 назначение перед циклами, но это все равно не сильно изменило результаты.

Кто-нибудь знает, почему первые несколько раз присваивание строк происходит медленнее? Я осмотрелся, но нашел только ответ, который произошел из-за JIT-компилятора, но C их не использует.

Комментарии:

1. Возможно, кэширование процессора? Загрузка данных по требованию?

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

Ответ №1:

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

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

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

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

Примечание: ваш метод измерения, усреднение, означает, что объем кэша служебных данных ( time_diffs массива) намного больше, чем у самого теста, так что, вероятно, это и является причиной нерегулярности, в первый раз time_diffs не кэшируется.

Комментарии:

1. Я никогда не говорил, что мой код копирует какие-либо данные. Какое это имеет отношение?

2. @TheRedFox Вы сказали «неважно, назначается ли 2 символа, 8 символов или сколько угодно».

Ответ №2:

Здесь есть два важных пункта:-

  • Не используйте отладочный код для синхронизации
  • Убедитесь, что вы действительно что-то рассчитываете

Первое очевидно, второе нет. Например, этот код:-

 for(i = 0; i < NUM_OF_LOOPS; i  ) {
    START_COUNTING;
    str = "T";
    END_COUNTING;
    time_diffs[i] = time_difference(end, start);
}
  

вполне вероятно, что это будет скомпилировано оптимизирующим компилятором:-

 for(i = 0; i < NUM_OF_LOOPS; i  ) {
    START_COUNTING;
    END_COUNTING;
    time_diffs[i] = time_difference(end, start);
}
str = "T";
  

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

     START_COUNTING;
    END_COUNTING;
  

и, надеюсь, код занесен в кэш, готовый к циклам синхронизации.