Почему я не должен перехватывать исключение Undefined Instruction вместо использования CPUID?

Предположим, я хочу использовать инструкцию, которая может быть недоступна. И эта инструкция не относится к тем прозрачным запасным вариантам, это неопределенная инструкция, когда она недоступна. Например, это popcnt.

Могу ли я вместо использования cpuid просто попробовать вызвать его?

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

Конечно, будет штраф за производительность, но только один раз. Какие-либо дополнительные недостатки этого подхода?


person Alex Guteniev    schedule 26.04.2020    source источник


Ответы (1)


Одной из основных трудностей является правильное выполнение первого вызова.

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

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

(Обработчики сигналов GNU/Linux получают нестандартное состояние сохраненного регистра потока, в котором они работают, поэтому теоретически вы можете сделать это там.)

Предположительно, это актуально только для предварительной компиляции; если вы используете JIT, вы должны просто проверить CPUID заранее, а не создавать пути обработки исключений.


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

Таким образом, единственным спасением здесь является одна простая функция инициализации, которую ваша программа запускает один раз, которая запускает CPUID пару раз и устанавливает все указатели функций. Выполнение этого позже лениво по мере необходимости означает больше промахов кэша, если только многие указатели функций не остаются неиспользованными. например large-program --help.

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


Вам также необходимо знать, какая инструкция вызвала ошибку, если ваша программа использует несколько функций ЦП.

Если вы эмулируете или что-то в этом роде, вам нужно будет проверить это, чтобы увидеть, не является ли это одной из ваших ожидаемых инструкций, которая может вызвать #UD execptions / сигналы SIGILL. например путем проверки машинного кода по адресу неисправности.

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

person Peter Cordes    schedule 26.04.2020
comment
Я собирался перезапустить цикл после того, как он потерпит неудачу, и не пытался определить, что именно потерпело неудачу. Так что это было бы не так сложно и неэффективно, как эмуляция. Конечно, тогда цикл должен быть написан так, чтобы все, что делается до потенциального сбоя, не наносило вреда дальнейшему выполнению. Но если бы я хотел эмулировать, это тоже выполнимо: на x64 MSVC в выражении __except вся эта информация доступна через GetExceptionInformation(), а также из обратного вызова AddVectoredExceptionHandler , и есть возможность вернуть EXCEPTION_CONTINUE_EXECUTION. - person Alex Guteniev; 26.04.2020
comment
@AlexGuteniev: хорошо, поэтому вам действительно понадобится попытка/поймать, которая имеет небольшие, но ненулевые накладные расходы в случае отсутствия исключений. По крайней мере, может, ограничивая возможности компилятора по оптимизации из-за возможности скачка. И ограничение его только циклами, которые можно прервать и повторить другим способом, позволяет избежать многих проблем. - person Peter Cordes; 26.04.2020
comment
@AlexGuteniev: Преимущество более быстрого запуска здесь кажется настолько незначительным для случая безотказной работы, что я не думаю, что на самом деле это стоит делать, даже если попытка/поймать не добавляет никаких накладных расходов на вызов. В Linux вам придется сделать системный вызов, чтобы установить обработчик сигналов для SIGILL, что потребует больше затрат при запуске, чем простое выполнение CPUID в пользовательском пространстве. Если Windows просто автоматически имеет SEH, то, возможно, все по-другому. Непереносимость подхода также является проблемой. - person Peter Cordes; 26.04.2020
comment
Я имею в виду, что нужно иметь всего две ветки: для процессоров программа работает в реальности и для процессоров, которые прописаны в минимальных требованиях. Так что вместо CPUIDing SSE2/3/4, POPCNT, чего угодно, просто перехватите все исключения недопустимых инструкций в одном месте. x64 SEH (в отличие от x86) претендует на почти нулевые накладные расходы, если исключение не происходит, поскольку он использует таблицы функций вместо выполняемой инструкции, специфичной для SEH. - person Alex Guteniev; 26.04.2020
comment
@AlexGuteniev: Если у вас есть специализированные версии для некоторых функций, некоторым может потребоваться только SSSE3, некоторым SSSE4.1, некоторым SSE4.2 и/или popcnt. И у вас может быть версия AVX + FMA или версия AVX2. Старые процессоры больше всего нуждаются в помощи, чтобы иметь приемлемую производительность для интерактивного использования, поэтому вы не хотите отказываться от версии SSSE3 чего-то важного только потому, что AVX недоступен для чего-то менее часто используемого. (например, процессоры без AVX все еще широко распространены, версии Pentium/Celeron текущих процессоров Intel. Также маломощные процессоры, такие как Goldmont.) - person Peter Cordes; 26.04.2020