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

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

    global = 0;
    for (int i = 0; i < SIZE; i++)
        global++;

, который использует глобальную переменную long global, против

    long tmp = 0;
    for (int i = 0; i < SIZE; i++)
        tmp++;
    global = tmp;

На уровне оптимизации -O0 время по существу равно (как я и ожидал), при -O1 оно несколько быстрее, но все равно равно, но по сравнению с -O2 версия, использующая глобальную переменную, намного быстрее (в 7 раз или около того).

С другой стороны, в следующих фрагментах кода, где старт указывает на блок байтов размером SIZE:

    global = 0;
    for (const char* p = start; p < start + SIZE; p++)
        global += *p;

против

    long tmp = 0;
    for (const char* p = start; p < start + SIZE; p++)
        tmp += *p;
    global = tmp;

Здесь при -O0 тайминги близкихотя версия, использующая локальную переменную, немного быстрее, что не кажется удивительным, поскольку, возможно, она будет храниться в регистре, тогда как global - нет.Тогда при -O1 и выше версия, использующая локальную переменную, значительно быстрее (более чем в 50% или в 1,5 раза).Как отмечалось ранее, это удивляет меня, потому что я думаю, что для gcc было бы так же легко, как для меня, использовать локальную переменную (в сгенерированном оптимизированном коде) для последующего присвоения глобальной переменной.

Итак, мой вопрос: что такое глобальные и локальные переменные, из-за которых gcc может выполнять только определенные оптимизации для одного типа, а не для другого?

Некоторые детали могут быть, а могут и не бытьуместно: я использовал gcc / g ++ версии 3.4.5 на машине с RHEL4 с двумя одноядерными процессорами и 4 ГБ ОЗУ.Значение, которое я использовал для SIZE, который является макросом препроцессора, было 1000000000. Блок байтов во втором примере был распределен динамически.

Вот некоторые выходные данные синхронизации для уровней оптимизации от 0 до 4 (в том же порядкекак указано выше):

$ ./st0
Result using global variable: 1000000000 in 2.213 seconds.
Result using local variable:  1000000000 in 2.210 seconds.
Result using global variable: 0 in 3.924 seconds.
Result using local variable:  0 in 3.710 seconds.
$ ./st1
Result using global variable: 1000000000 in 0.947 seconds.
Result using local variable:  1000000000 in 0.947 seconds.
Result using global variable: 0 in 2.135 seconds.
Result using local variable:  0 in 1.212 seconds.
$ ./st2
Result using global variable: 1000000000 in 0.022 seconds.
Result using local variable:  1000000000 in 0.552 seconds.
Result using global variable: 0 in 2.135 seconds.
Result using local variable:  0 in 1.227 seconds.
$ ./st3
Result using global variable: 1000000000 in 0.065 seconds.
Result using local variable:  1000000000 in 0.461 seconds.
Result using global variable: 0 in 2.453 seconds.
Result using local variable:  0 in 1.646 seconds.
$ ./st4
Result using global variable: 1000000000 in 0.063 seconds.
Result using local variable:  1000000000 in 0.468 seconds.
Result using global variable: 0 in 2.467 seconds.
Result using local variable:  0 in 1.663 seconds.

РЕДАКТИРОВАТЬ Это сгенерированная сборка для первых двух фрагментов с переключателем -O2, случай, когда разница является наибольшей.Насколько я понимаю, это выглядит как ошибка в компиляторе: 0x3b9aca00 - это SIZE в шестнадцатеричном формате, 0x80496dc должен быть глобальным адресом.Я проверил с новым компилятором, и это больше не происходит.Разница во второй паре фрагментов похожа, однако.

    void global1()
    {
        int i;
        global = 0;
        for (i = 0; i < SIZE; i++)
            global++;
    }

    void local1()
    {
        int i;
        long tmp = 0;
        for (i = 0; i < SIZE; i++)
            tmp++;
        global = tmp;
    }

    080483d0 <global1>:
     80483d0:   55                      push   %ebp
     80483d1:   89 e5                   mov    %esp,%ebp
     80483d3:   c7 05 dc 96 04 08 00    movl   $0x0,0x80496dc
     80483da:   00 00 00 
     80483dd:   b8 ff c9 9a 3b          mov    $0x3b9ac9ff,%eax
     80483e2:   89 f6                   mov    %esi,%esi
     80483e4:   83 e8 19                sub    $0x19,%eax
     80483e7:   79 fb                   jns    80483e4 <global1+0x14>
     80483e9:   c7 05 dc 96 04 08 00    movl   $0x3b9aca00,0x80496dc
     80483f0:   ca 9a 3b 
     80483f3:   c9                      leave  
     80483f4:   c3                      ret    
     80483f5:   8d 76 00                lea    0x0(%esi),%esi

    080483f8 <local1>:
     80483f8:   55                      push   %ebp
     80483f9:   89 e5                   mov    %esp,%ebp
     80483fb:   b8 ff c9 9a 3b          mov    $0x3b9ac9ff,%eax
     8048400:   48                      dec    %eax
     8048401:   79 fd                   jns    8048400 <local1+0x8>
     8048403:   c7 05 dc 96 04 08 00    movl   $0x3b9aca00,0x80496dc
     804840a:   ca 9a 3b 
     804840d:   c9                      leave  
     804840e:   c3                      ret    
     804840f:   90                      nop    

Наконец, вот код оставшихся фрагментов, которые теперь генерируются gcc 4.3.3 с использованием -O3 (хотя старая версия, похоже, генерирует аналогичныекод).Похоже, что global2 (..) компилируется в функцию, получающую доступ к глобальной памяти в каждой итерации цикла, где local2 (..) использует регистр.Мне до сих пор не ясно, почему gcc все равно не оптимизирует глобальную версию, используя регистр.Это просто отсутствующая функция, или она действительно приведет к недопустимому поведению исполняемого файла?

    void global2(const char* start)
    {
        const char* p;
        global = 0;
        for (p = start; p < start + SIZE; p++)
            global += *p;
    }

    void local2(const char* start)
    {
        const char* p;
        long tmp = 0;
        for (p = start; p < start + SIZE; p++)
            tmp += *p;
        global = tmp;
    }

    08048470 <global2>:
     8048470:   55                      push   %ebp
     8048471:   31 d2                   xor    %edx,%edx
     8048473:   89 e5                   mov    %esp,%ebp
     8048475:   8b 4d 08                mov    0x8(%ebp),%ecx
     8048478:   c7 05 24 a0 04 08 00    movl   $0x0,0x804a024
     804847f:   00 00 00 
     8048482:   8d b6 00 00 00 00       lea    0x0(%esi),%esi
     8048488:   0f be 04 11             movsbl (%ecx,%edx,1),%eax
     804848c:   83 c2 01                add    $0x1,%edx
     804848f:   01 05 24 a0 04 08       add    %eax,0x804a024
     8048495:   81 fa 00 ca 9a 3b       cmp    $0x3b9aca00,%edx
     804849b:   75 eb                   jne    8048488 <global2+0x18>
     804849d:   5d                      pop    %ebp
     804849e:   c3                      ret    
     804849f:   90                      nop    

    080484a0 <local2>:
     80484a0:   55                      push   %ebp
     80484a1:   31 c9                   xor    %ecx,%ecx
     80484a3:   89 e5                   mov    %esp,%ebp
     80484a5:   31 d2                   xor    %edx,%edx
     80484a7:   53                      push   %ebx
     80484a8:   8b 5d 08                mov    0x8(%ebp),%ebx
     80484ab:   90                      nop    
     80484ac:   8d 74 26 00             lea    0x0(%esi,%eiz,1),%esi
     80484b0:   0f be 04 13             movsbl (%ebx,%edx,1),%eax
     80484b4:   83 c2 01                add    $0x1,%edx
     80484b7:   01 c1                   add    %eax,%ecx
     80484b9:   81 fa 00 ca 9a 3b       cmp    $0x3b9aca00,%edx
     80484bf:   75 ef                   jne    80484b0 <local2+0x10>
     80484c1:   5b                      pop    %ebx
     80484c2:   89 0d 24 a0 04 08       mov    %ecx,0x804a024
     80484c8:   5d                      pop    %ebp
     80484c9:   c3                      ret    
     80484ca:   8d b6 00 00 00 00       lea    0x0(%esi),%esi

Спасибо.

Ответы [ 2 ]

9 голосов
/ 30 августа 2011

Глобальная переменная = глобальная память и псевдонимы (читается как: плохо для оптимизатора - в худшем случае должен быть прочитан, изменен и записан).

Локальная переменная = регистр (если компилятор действительно не может помочь, иногда он также должен поместить его в стек, но стек практически гарантированно находится в L1)

Доступ к регистру осуществляется в порядке одного цикла, доступ к памяти - порядка 15-1000 циклов (в зависимости от того, находится ли строка кэша в кэше и не аннулирована ли другим ядром, и в зависимости от того, является ли страница в TLB).

9 голосов
/ 30 августа 2011

Локальная переменная tmp, адрес которой не взят, не может быть указана указателем p, и компилятор может оптимизировать ее соответствующим образом. Гораздо сложнее сделать вывод, что глобальная переменная global не указана, если только она не static, потому что адрес этой глобальной переменной может быть взят в другом модуле компиляции и передан вокруг.

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

Наконец, обратите внимание, что если бы указатель p был другого типа, компилятор мог бы вызывать «строгие правила псевдонимов» для оптимизации независимо от его неспособности сделать вывод, что p не указывает на global. Но поскольку lvalues ​​типа char часто используются для наблюдения за представлением других типов, для такого типа псевдонимов есть допуск, и компилятор не может использовать этот ярлык в вашем примере.

...