Как я могу указать, что память, на которую указывает * встроенный аргумент ASM, может использоваться?

Рассмотрим следующую маленькую функцию:

void foo(int* iptr) {
    iptr[10] = 1;
    __asm__ volatile ("nop"::"r"(iptr):);
    iptr[10] = 2;
}

Используя gcc, он компилируется в:

foo:
        nop
        mov     DWORD PTR [rdi+40], 2
        ret

Обратите внимание, в частности, что первая запись в iptr, iptr[10] = 1 не происходит вообще: встроенный асм nop это первая вещь в функции, и только последняя запись 2 появляется (после вызова ASM). По-видимому, компилятор решает, что ему нужно только предоставить обновленную версию значения iptr сам, но не память, на которую он указывает.

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

void foo(int* iptr) {
    iptr[10] = 1;
    __asm__ volatile ("nop"::"r"(iptr):"memory");
    iptr[10] = 2;
}

что приводит к ожидаемому коду:

foo:
        mov     DWORD PTR [rdi+40], 1
        nop
        mov     DWORD PTR [rdi+40], 2
        ret

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

void foo2(int* iptr, long* lptr) {
    iptr[10] = 1;
    lptr[20] = 100;
    __asm__ volatile ("nop"::"r"(iptr):);
    iptr[10] = 2;
    lptr[20] = 200;
}

Желаемое поведение - позволить компилятору оптимизировать первую запись в lptr[20], но не первым пишите iptr[10], "memory" clobber не может этого достичь, потому что это означает, что обе записи должны произойти:

foo2:
        mov     DWORD PTR [rdi+40], 1
        mov     QWORD PTR [rsi+160], 100 ; lptr[10] written unecessarily
        nop
        mov     DWORD PTR [rdi+40], 2
        mov     QWORD PTR [rsi+160], 200
        ret

Есть ли какой-нибудь способ сообщить компиляторам, принимающим расширенный синтаксис asm gcc, что вход в asm включает указатель и все, на что он может указывать?

1 ответ

Решение

Это правильно; запрос указателя в качестве входных данных для встроенного asm не означает, что указанная память также является входом или выходом или и тем, и другим. Имея входной регистр и выходной регистр, gcc знает, что ваш asm просто выравнивает указатель, маскируя младшие биты, или добавляет к нему константу. (В этом случае вы хотите оптимизировать мертвый магазин.)

Простой вариант asm volatile и "memory" клоббер1.

Более узкий, более конкретный способ, который вы запрашиваете, - это использовать "фиктивный" операнд памяти, а также указатель в регистре. Ваш шаблон asm не ссылается на этот операнд (за исключением, может быть, внутри комментария asm, чтобы увидеть, что выбрал компилятор). Он сообщает компилятору, какую память вы на самом деле читаете, пишете или читаете + пишете.

Пустой ввод памяти: "m" (*(const int (*)[]) iptr)
или вывод:"=m" (*(int (*)[]) iptr), Или конечно"+m"с тем же синтаксисом.

Этот синтаксис преобразуется в указатель на массив и разыменовывается, поэтому фактическим вводом являетсямассив C. (Если у вас действительно есть массив, а не указатель, вам не нужно ничего приводить и вы можете просто запросить его как операнд памяти.)

Если вы оставите размер без указания[], это говорит GCC, что любая память, к которой обращаются относительно этого указателя, является операндом ввода, вывода или ввода / вывода. Если вы используете [10] или же [some_variable], который сообщает компилятору конкретный размер. С размерами переменных во время выполнения gcc на практике пропускает оптимизацию, iptr[size+1] не является частью ввода.

GCC это документирует и поэтому поддерживает. Я думаю, что это не строгое нарушение псевдонимов, если тип элемента массива совпадает с указателем, или, если это char,

(из руководства GCC)
Пример x86, где строковый аргумент памяти имеет неизвестную длину.

   asm("repne scasb"
    : "=c" (count), "+D" (p)
    : "m" (*(const char (*)[]) p), "0" (-1), "a" (0));

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

Но если вы используете ранний клоббер для строгой правильности цикла asm, иногда фиктивный операнд будет выполнять инструкции gcc-траты (и дополнительный регистр) на базовом адресе для операнда памяти. Проверьте вывод asm компилятора.


Фон:

Это распространенная ошибка в примерах inline-asm, которая часто остается незамеченной, потому что asm обернут в функцию, которая не встраивается в какие-либо вызывающие объекты, которые побуждают компилятор переупорядочивать хранилища для слияния, выполняя удаление мертвых хранилищ.

Встроенный синтаксис asm GNU C разработан для описания одной инструкции для компилятора. Намерение состоит в том, чтобы вы сообщали компилятору о вводе памяти или выводе памяти с помощью "m" или же "=m" ограничение операнда, и он выбирает режим адресации.

Написание целых циклов во встроенном asm требует осторожности, чтобы убедиться, что компилятор действительно знает, что происходит (или asm volatile плюс "memory" clobber), в противном случае вы рискуете выйти из строя при изменении окружающего кода или включении оптимизации по времени компоновки, которая допускает встраивание между файлами.

См. Также зацикливание массивов со встроенной сборкой для использования asm оператор как тело цикла, все еще делая логику цикла в C. С фактическим (не фиктивным) "m" а также "=m" Операнды, компилятор может развернуть цикл, используя смещения в выбранных им режимах адресации.


Сноска 1: A "memory" clobber заставляет компилятор обрабатывать asm как вызов не встроенной функции (который может читать или записывать любую память, кроме локальных, которые, как доказал экранирующий анализ, не избежали). Экранирующий анализ включает в себя входные операнды самого оператора asm, а также любые глобальные или статические переменные, в которых любой предыдущий вызов мог хранить указатели. Поэтому обычно локальные счетчики циклов не нужно разливать / перезагружать вокруг asm заявление с "memory" тряпки.

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

Или для памяти, которая читается только asm, вам нужно запустить asm снова, если один и тот же входной буфер содержит разные входные данные. Без volatileоператор asm может быть CSEd вне цикла. (A "memory" Clobber не заставляет оптимизатор обрабатывать всю память как входные данные при рассмотрении asm Заявление даже нужно запустить.)

asm без выходных операндов неявно volatile, но это хорошая идея, чтобы сделать это явным. (В руководстве GCC есть раздел, посвященный asm volatile).

например asm("... sum an array ..." : "=r"(sum) : "r"(pointer), "r"(end_pointer) : "memory") имеет выходной операнд, поэтому не является неявно изменчивым. Если вы использовали это как

 arr[5] = 1;
 total += asm_sum(arr, len);
 memcpy(arr, foo, len);
 total += asm_sum(arr, len);

Без volatile 2-й asm_sum может оптимизировать, предполагая, что один и тот же asm с одинаковыми входными операндами (указатель и длина) будет производить одинаковый вывод. Тебе нужно volatile для любого asm это не является чистой функцией его явных входных операндов. Если это не оптимизировать, то "memory" Clobber будет иметь желаемый эффект, требующий синхронизации памяти.

Другие вопросы по тегам