Поведение после приращения - PullRequest
0 голосов
/ 08 июня 2018

Недавно я обновил свой проект с gcc 4.3 до gcc 5.5.После этого я вижу изменение в поведении постинкрементного оператора, которое вызывает проблемы в моем проекте.Я использую глобальную переменную в качестве управляющей переменной.Например, рассмотрим этот пример программы:

int i = 0;
int main()
{
        int x[10];

        x[i++] = 5; ===> culprit

        return 0;

}

В приведенном выше фрагменте значение i следует увеличивать только после того, как 5 назначено x[0], что будет гарантировать, что x[0]имеет правильное действительное значение, назначенное до увеличения i.

Теперь возникает проблема, я вижу, что после перехода на gcc 5.5 инструкции по сборке изменились и значение i увеличивается на единицу еще до назначенияслучилось.Инструкция по сборке из приведенного выше фрагмента:

Dump of assembler code for function main():
6       {
   0x0000000000400636 <+0>:     push   %rbp
   0x0000000000400637 <+1>:     mov    %rsp,%rbp

7               int x[10];
8
9               x[i++] = 1;
   0x000000000040063a <+4>:     mov    0x200a00(%rip),%eax        # 0x601040 <i>
   0x0000000000400640 <+10>:    lea    0x1(%rax),%edx
   0x0000000000400643 <+13>:    mov    %edx,0x2009f7(%rip)        # 0x601040 <i>  ====> i gets incremented here
   0x0000000000400649 <+19>:    cltq
   0x000000000040064b <+21>:    movl   $0x5,-0x30(%rbp,%rax,4)    =====> x[0] is assigned value here

10
11              return 0;
   0x0000000000400653 <+29>:    mov    $0x0,%eax

12
13      }
   0x0000000000400658 <+34>:    pop    %rbp
   0x0000000000400659 <+35>:    retq

Из-за описанной выше сборки другой поток внутри процесса, использующий переменную i, начинает читать неправильные значения из глобального массива.

Теперь тот же кодпри компиляции с использованием gcc 4.3 придерживается поведения, которое я понимаю, т.е. сначала присваивается значение, а затем i увеличивается.Инструкция по сборке с использованием gcc 4.3 для того же фрагмента:

Dump of assembler code for function main():
5       int main()
   0x00000000004005da <+0>:     push   %rbp
   0x00000000004005db <+1>:     mov    %rsp,%rbp

6       {
7               int x[10];
8
9               x[i++] = 1;
   0x00000000004005de <+4>:     mov    0x200a64(%rip),%edx        # 0x601048 <i>
   0x00000000004005e4 <+10>:    movslq %edx,%rax
   0x00000000004005e7 <+13>:    movl   $0x5,-0x30(%rbp,%rax,4)     ======> x[0] gets assigned here
   0x00000000004005ef <+21>:    lea    0x1(%rdx),%eax
   0x00000000004005f2 <+24>:    mov    %eax,0x200a50(%rip)        # 0x601048 <i>   ======> i gets incremented here

10
11              return 0;
   0x00000000004005f8 <+30>:    mov    $0x0,%eax

12
13      }
   0x00000000004005fd <+35>:    leaveq
   0x00000000004005fe <+36>:    retq

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

Буду благодарен за любую помощь или подсказки.

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

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

Ответы [ 2 ]

0 голосов
/ 08 июня 2018

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

Кроме того, при изменении более чем одной переменной необходимо обеспечить произойдет до упорядочение памяти семантика, чтобы другой поток «видел» новые значения в правильном порядке во времени.Использование mutex автоматически обеспечивает семантику получения-освобождения (т. Е. Все, что произошло в одном потоке перед освобождением мьютекса, будет видно потоку, который снова его блокирует).Без упорядочения памяти у вас нет гарантии, когда потоки будут видеть изменения друг друга во времени (или вообще).

atomic также поставляется с упорядочением памяти, но только для самой переменной.

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

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

0 голосов
/ 08 июня 2018

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

Компилятор может свободно преобразовывать код в:

int temp = i;
i = temp+1;
x[temp] = 5;

Кроме того, если у вас есть один поток, изменяющий i, а другой поток читает i, у вас нет гарантии, какое значение увидит другой поток.Вы не можете даже получить гарантию, что он увидит либо новое, либо старое значение, если i не равно std::atomic.

Если вы пытаетесь обновить оба i и x скоординированным образом, вам понадобится блокировка.

Компилятор может даже преобразовать ваш код в:

i = i + 1;
// <<<<
x[i-1] = 5;

Что интересно, если другой поток запуститсяи изменяет i в точке, отмеченной <<<<.

...