Информация об изменениях

Сообщение Re[5]: Когда это наконец станет defined behavior? от 29.04.2023 15:42

Изменено 29.04.2023 15:56 netch80

Re[5]: Когда это наконец станет defined behavior?
Здравствуйте, T4r4sB, Вы писали:

N>> Я по умолчанию бы предпочёл видеть все операции с памятью как неявные барьеры


TB>То есть в цикле

TB>
TB>while (*dst++ = *src++);
TB>

TB>Значит каждый раз не только читать содержимое указателей src,dst, но и каждый раз читать содержимое куска памяти, где лежат значения этих указателей?

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

По пунктам:

1. Если src и dst это локальные переменные функции, и никто не берёт их адрес для каких-то целей (или делает это позже данного кода), то то, что я говорю, на них не распространяется. Эти переменные могут быть в стеке, в регистре, прыгать туда-обратно по настроению компилятора — неважно.
Аргументы функции в этом смысле включаются в локальные переменные — если они не приняты по ссылке (а нахрена?)

2. А вот содержимое памяти по этим указателям — да, должен читать. И ровно это, если ты имеешь в виду логику strcpy (ты ведь зачем-то такой пример взял, да?), происходит и сейчас. Компилятор обязан читать и писать память под указателями, причём даже по двум причинам: указатели одинакового типа — значит, логика алиасинга "память под указателями разных типов независима", и указатели типа char* — что убивает независимость операций (есть такое специальное правило).

3. Я что-то не понял отношения между частями твоего вопроса. Если бы это было "не только читать содержимое куска памяти... но и читать содержимое указателей", было бы понятно. А так — нет.

То, что я имел в виду изначально, это вещи следующего вида.
Самое простое: если мы видим, например,

int a, b, c; // глобальные
void foo() {
  b = a;
  c = a;
}


то объединять их не положено, пока не будет разрешено, например, в стиле

[[aliasing(relaxed)]]
void foo() {
  b = a;
  c = a;
}


Или, например, у нас есть код:

void boo(float *a, int n, int *c) {
  for (int i = 0; i < n; ++i) { a[i] *= *c; }
}


Сейчас за счёт разнотипности *a и *c считается, что присвоение любому a[i] не может влиять на значение *c, поэтому при любой оптимизации значение *c начинает кэшироваться.
Берём тот же gcc:

  asm1
boo:
.LFB0:
        testl   %esi, %esi
        jle     .L1
        movq    %rdi, %rax
        leal    -1(%rsi), %ecx
        leaq    4(%rdi,%rcx,4), %rcx
.L3:
        pxor    %xmm0, %xmm0
        cvtsi2ssl       (%rdx), %xmm0 <-- Вот тут один раз прочитал и запомнил
        mulss   (%rax), %xmm0
        movss   %xmm0, (%rax)
        addq    $4, %rax
        cmpq    %rcx, %rax
        jne     .L3
.L1:
        ret


А теперь меняем int *c на float *c, и это разрешение компилятору уходит, потому что он уже подозревает, что присвоение a[i] может повлиять на *c:

  asm2
boo:
        testl   %esi, %esi
        jle     .L1
        movq    %rdi, %rax
        leal    -1(%rsi), %ecx
        leaq    4(%rdi,%rcx,4), %rcx
.L3:
        movss   (%rax), %xmm0
        mulss   (%rdx), %xmm0 <-- Читает на каждой итерации
        movss   %xmm0, (%rax)
        addq    $4, %rax
        cmpq    %rcx, %rax
        jne     .L3
.L1:
        ret


А теперь я добавляю слово restrict (увы, только C, не C++) к float *c, и оно возвращается к однократному чтению:

  asm3
boo:
        testl   %esi, %esi
        jle     .L1
        movss   (%rdx), %xmm1 <-- Прочли один раз и запомнили
        movq    %rdi, %rax
        leal    -1(%rsi), %edx
        leaq    4(%rdi,%rdx,4), %rdx
.L3:
        movaps  %xmm1, %xmm0
        mulss   (%rax), %xmm0
        movss   %xmm0, (%rax)
        addq    $4, %rax
        cmpq    %rdx, %rax
        jne     .L3
.L1:
        ret


Вот то что я хочу видеть по умолчанию — это такой себе anti-restrict (как в asm2) для всех операций с памятью (то есть с любым, что не является локальной переменной, у которой не брали адрес), а кроме того запрет переупорядочения доступа к ним.
В примере 2, если кто-то считает при этом, что недостаточно скорости для доступа к *c, то он может сложить в локальную переменную — или таки атрибутами выставить облегчение, ослабив тотальный алиасинг — в пределах одного куска исходного кода.

Надеюсь, теперь идея понятна?
Re[5]: Когда это наконец станет defined behavior?
Здравствуйте, T4r4sB, Вы писали:

N>> Я по умолчанию бы предпочёл видеть все операции с памятью как неявные барьеры


TB>То есть в цикле

TB>
TB>while (*dst++ = *src++);
TB>

TB>Значит каждый раз не только читать содержимое указателей src,dst, но и каждый раз читать содержимое куска памяти, где лежат значения этих указателей?

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

По пунктам:

1. Если src и dst это локальные переменные функции, и никто не берёт их адрес для каких-то целей (или делает это позже данного кода), то то, что я говорю, на них не распространяется. Эти переменные могут быть в стеке, в регистре, прыгать туда-обратно по настроению компилятора — неважно.
Аргументы функции в этом смысле включаются в локальные переменные — если они не приняты по ссылке (а нахрена?)

2. А вот содержимое памяти по этим указателям — да, должен читать. И ровно это, если ты имеешь в виду логику strcpy (ты ведь зачем-то такой пример взял, да?), происходит и сейчас. Компилятор обязан читать и писать память под указателями, причём даже по двум причинам: указатели одинакового типа — значит, логика алиасинга "память под указателями разных типов независима", и указатели типа char* — что убивает независимость операций (есть такое специальное правило).

3. Я что-то не понял отношения между частями твоего вопроса. Если бы это было "не только читать содержимое куска памяти... но и читать содержимое указателей", было бы понятно. А так — нет.

То, что я имел в виду изначально, это вещи следующего вида.
Самое простое: если мы видим, например,

int a, b, c; // глобальные
void foo() {
  b = a;
  c = a;
}


то объединять их не положено, пока не будет разрешено, например, в стиле

[[aliasing(relaxed)]]
void foo() {
  b = a;
  c = a;
}


Или, например, у нас есть код:

void boo(float *a, int n, int *c) {
  for (int i = 0; i < n; ++i) { a[i] *= *c; }
}


Сейчас за счёт разнотипности *a и *c считается, что присвоение любому a[i] не может влиять на значение *c, поэтому при любой оптимизации значение *c начинает кэшироваться.
Берём тот же gcc:

  asm1
boo:
.LFB0:
        testl   %esi, %esi
        jle     .L1
        pxor    %xmm1, %xmm1
        leal    -1(%rsi), %eax
        cvtsi2ssl       (%rdx), %xmm1 <-- Вот тут один раз прочитал и запомнил
        leaq    4(%rdi,%rax,4), %rax
.L3:
        movss   (%rdi), %xmm0
        addq    $4, %rdi
        mulss   %xmm1, %xmm0
        movss   %xmm0, -4(%rdi)
        cmpq    %rax, %rdi
        jne     .L3
.L1:
        ret


А теперь меняем int *c на float *c, и это разрешение компилятору уходит, потому что он уже подозревает, что присвоение a[i] может повлиять на *c:

  asm2
boo:
        testl   %esi, %esi
        jle     .L1
        movq    %rdi, %rax
        leal    -1(%rsi), %ecx
        leaq    4(%rdi,%rcx,4), %rcx
.L3:
        movss   (%rax), %xmm0
        mulss   (%rdx), %xmm0 <-- Читает на каждой итерации
        movss   %xmm0, (%rax)
        addq    $4, %rax
        cmpq    %rcx, %rax
        jne     .L3
.L1:
        ret


А теперь я добавляю слово restrict (увы, только C, не C++) к float *c, и оно возвращается к однократному чтению:

  asm3
boo:
        testl   %esi, %esi
        jle     .L1
        movss   (%rdx), %xmm1 <-- Прочли один раз и запомнили
        movq    %rdi, %rax
        leal    -1(%rsi), %edx
        leaq    4(%rdi,%rdx,4), %rdx
.L3:
        movaps  %xmm1, %xmm0
        mulss   (%rax), %xmm0
        movss   %xmm0, (%rax)
        addq    $4, %rax
        cmpq    %rdx, %rax
        jne     .L3
.L1:
        ret


Вот то что я хочу видеть по умолчанию — это такой себе anti-restrict (как в asm2) для всех операций с памятью (то есть с любым, что не является локальной переменной, у которой не брали адрес), а кроме того запрет переупорядочения доступа к ним.
В примере 2, если кто-то считает при этом, что недостаточно скорости для доступа к *c, то он может сложить в локальную переменную — или таки атрибутами выставить облегчение, ослабив тотальный алиасинг — в пределах одного куска исходного кода.

Надеюсь, теперь идея понятна?