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

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

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 вообще не происходит: встроенный asm nop является первым в функции, и появляется только последняя запись 2 (после вызова ASM).По-видимому, компилятор решает, что ему нужно предоставить только актуальную версию значения iptr самого , но не памяти, на которую он указывает.

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

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 Ответ

7 голосов
/ 04 июня 2019

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

Простая опция: asm volatile и "memory" clobber 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 output компилятора.


Background:

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

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

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

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


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

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

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

asm без выходных операндов неявно volatile, но это хорошая идея, чтобы сделать его явным. (В руководстве GCC есть раздел as 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" будет иметь желаемый эффект, требующий синхронизации памяти.

...