Глобальные переменные замедляют код

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

for(int i = 0; i < N; ++i)
    tan(tan(tan(tan(tan(tan(tan(tan(x++))))))));
end
std::cout << x;

где N - глобальная переменная, выполняется значительно медленнее:

int N = 10000;
for(int i = 0; i < N; ++i)
    tan(tan(tan(tan(tan(tan(tan(tan(x++))))))));
end
std::cout << x;

Что происходит с глобальной переменной, которая заставляет ее работать медленнее?

Ответ 1

tl; dr: локальная версия хранит N в регистре, а глобальная версия - нет. Объявляйте константы с константой, и она будет быстрее, независимо от того, как вы ее объявите.


Вот пример кода, который я использовал:

#include <iostream>
#include <math.h>
void first(){
  int x=1;
  int N = 10000;
  for(int i = 0; i < N; ++i)
    tan(tan(tan(tan(tan(tan(tan(tan(x++))))))));
  std::cout << x;
}
int N=10000;
void second(){
  int x=1;
  for(int i = 0; i < N; ++i)
    tan(tan(tan(tan(tan(tan(tan(tan(x++))))))));
  std::cout << x;
}
int main(){
  first();
  second();
}

(с именем test.cpp).

Чтобы посмотреть на сгенерированный код ассемблера, я запустил g++ -S test.cpp.

У меня огромный файл, но с некоторым умным поиском (я искал загар), я нашел то, что хотел:

из функции first:

Ltmp2:
    movl    $1, -4(%rbp)
    movl    $10000, -8(%rbp) ; N is here !!!
    movl    $0, -12(%rbp)    ;initial value of i is here
    jmp LBB1_2       ;goto the 'for' code logic
LBB1_1:             ;the loop is this segment
    movl    -4(%rbp), %eax
    cvtsi2sd    %eax, %xmm0
    movl    -4(%rbp), %eax
    addl    $1, %eax
    movl    %eax, -4(%rbp)
    callq   _tan
    callq   _tan
    callq   _tan
    callq   _tan
    callq   _tan        
    callq   _tan
    callq   _tan
    movl    -12(%rbp), %eax
    addl    $1, %eax
    movl    %eax, -12(%rbp) 
LBB1_2:
    movl    -12(%rbp), %eax ;value of n kept in register 
    movl    -8(%rbp), %ecx  
    cmpl    %ecx, %eax  ;comparing N and i here
    jl  LBB1_1      ;if less, then go into loop code
    movl    -4(%rbp), %eax

вторая функция:

Ltmp13:
    movl    $1, -4(%rbp)    ;i
    movl    $0, -8(%rbp) 
    jmp LBB5_2
LBB5_1:             ;loop is here
    movl    -4(%rbp), %eax
    cvtsi2sd    %eax, %xmm0
    movl    -4(%rbp), %eax
    addl    $1, %eax
    movl    %eax, -4(%rbp)
    callq   _tan
    callq   _tan
    callq   _tan
    callq   _tan
    callq   _tan
    callq   _tan
    callq   _tan
    movl    -8(%rbp), %eax
    addl    $1, %eax
    movl    %eax, -8(%rbp)
LBB5_2:
    movl    _N(%rip), %eax  ;loading N from globals at every iteration, instead of keeping it in a register
    movl    -8(%rbp), %ecx

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

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

если вы добавите const в объявление N (const int N=10000), это будет даже быстрее, чем локальная версия:

    movl    -8(%rbp), %eax
    addl    $1, %eax
    movl    %eax, -8(%rbp)
LBB5_2:
    movl    -8(%rbp), %eax
    cmpl    $9999, %eax ;9999 used instead of 10000 for some reason I do not know
    jle LBB5_1

N заменяется константой.

Ответ 2

Глобальная версия не может быть оптимизирована для размещения ее в регистре.

Ответ 3

Я немного экспериментировал с вопросом и ответом @rtpg,

экспериментируя с вопросом

В файле main1.h глобальная переменная N

int N = 10000;

Затем в файле main1.c 1000 вычислений ситуации:

#include <stdio.h>
#include "sys/time.h"
#include "math.h"
#include "main1.h"



extern int N;

int main(){

        int k = 0;
        timeval static_start, static_stop;
        int x = 0;

        int y = 0;
        timeval start, stop;
        int M = 10000;

        while(k <= 1000){

                gettimeofday(&static_start, NULL);
                for (int i=0; i<N; ++i){
                        tan(tan(tan(tan(tan(tan(tan(tan(x++))))))));
                }
                gettimeofday(&static_stop, NULL);

                gettimeofday(&start, NULL);
                for (int j=0; j<M; ++j){
                        tan(tan(tan(tan(tan(tan(tan(tan(y++))))))));
                }
                gettimeofday(&stop, NULL);

                int first_interval = static_stop.tv_usec - static_start.tv_usec;
                int last_interval = stop.tv_usec - start.tv_usec;

                if(first_interval >=0 && last_interval >= 0){
                        printf("%d, %d\n", first_interval, last_interval);
                }

                k++;
        }

        return 0;
}

Результаты показаны в следующей гистограмме (частота/микросекунды):

the histogram for the comparison output time in both methods Красные прямоугольники - это не глобальная переменная, основанная на контуре (N), а прозрачная зеленая M заканчивается на основе цикла (не глобальная).

Есть доказательства того, что глобальный varialbe extern немного медленный.

экспериментируя с ответом Причина @rtpg очень сильна. В этом смысле глобальная переменная может быть медленнее.

Скорость доступа к локальным и глобальным переменным в gcc/g++ на разных уровнях оптимизации

Чтобы проверить эту предпосылку, я использую глобальную переменную регистра для проверки производительности. Это был мой main1.h с глобальной переменной

int N asm ("myN") = 10000;

Новая гистограмма результатов:

Results with register global variable

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

Ответ 4

Я предполагаю, что оптимизатор не знает содержимое функции tan при компиляции вышеуказанного кода.

То, что tan делает, неизвестно - все, что он знает, это набивать вещи в стек, переходить на какой-то адрес, а затем очищать стек потом.

В случае глобальной переменной компилятор не знает, что tan делает для N. В локальном случае нет "свободных" указателей или ссылок на N, на которые tan мог бы законно получить: так что компилятор знает, какие значения N будут приняты.

Компилятор может сгладить петлю - от полностью (один плоский блок из 10000 строк), частично (100 циклов длины, каждый со 100 линиями) или совсем нет (длина 10000 циклов по 1 строке каждая) или ничего между ними.

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

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

Ответ 5

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