Какие замены доступны для ранее поддерживаемых моделей поведения, не определенных стандартом C
В первые дни C до стандартизации реализации имели множество способов обработки исключительных и полуизбыточных случаев различных действий. Некоторые из них вызовут ловушки, которые могут привести к случайному выполнению кода, если они не настроены в первую очередь. Поскольку поведение таких ловушек выходит за рамки стандарта C (и в некоторых случаях может контролироваться операционной системой вне контроля запущенной программы) и избегать того, чтобы компиляторы не допускали код, который полагался на такие ловушек, чтобы продолжать это делать, поведение действий, которые могли бы вызвать такие ловушки, оставалось полностью на усмотрение компилятора/платформы.
К концу 1990-х годов, хотя и не требовалось сделать это по стандарту C, каждый основной компилятор принимал общее поведение для многих из этих ситуаций; использование такого поведения позволило бы улучшить скорость, размер и читаемость кода.
Поскольку "очевидные" способы запроса следующих операций больше не поддерживаются, как следует их заменять таким образом, чтобы не препятствовать читаемости и не влиять на генерацию кода при использовании более старых компиляторов? Для целей описания предположим, что int
- 32-разрядный, ui
- это unsigned int, si
- подпись int, а b
- unsigned char.
-
Учитывая ui
и b
, вычислите ui << b
для b == 0..31 или значение, которое может произвольно вести себя как ui << (b & 31)
или ноль для значений 32..255. Обратите внимание: если левый операнд равен нулю всякий раз, когда правый операнд превышает 31, оба поведения будут одинаковыми.
-
Для кода, который должен выполняться только на процессоре, который дает нуль при сдвиге вправо или влево на величину от 32 до 255, вычислить ui << b
для b == 0..31 и 0 для б == 32..255. Хотя компилятор может оптимизировать условную логику, предназначенную для пропуска сдвига для значений 32..255 (поэтому код просто выполнил бы сдвиг, который даст правильное поведение), я не знаю, каким образом можно сформулировать такую условную логику что гарантирует, что компилятор не будет генерировать ненужный код для него.
-
Как и для 1 и 2, но для сдвигов вправо.
-
Учитывая si
и b
, что b0..30 и si*(1<<b)
не будут переполняться, вычислите si*(1<<b)
. Обратите внимание, что использование оператора умножения сильно ухудшит производительность многих старых компиляторов, но если целью сдвига является масштабирование знакового значения, то приведение в неподписанное значение в случаях, когда операнд будет оставаться отрицательным во время сдвига, кажется неправильным.
-
Учитывая различные целочисленные значения, выполняйте добавления, вычитания, умножения и сдвиги, таким образом, чтобы при отсутствии переполнения результаты были правильными, и если есть переполнения, код будет либо генерировать значения, верхние биты которых ведут себя в не-ловушке и не-UB, но в противном случае неопределенном способе или ловушке распознаваемой платформы (и на платформах, которые не поддерживают ловушки, просто даст неопределенное значение).
-
Указав указатель на выделенную область и некоторые указатели на вещи внутри нее, используйте realloc
, чтобы изменить размер выделения и настроить вышеупомянутые указатели, чтобы они соответствовали, избегая при этом дополнительной работы в случаях, когда realloc
возвращает оригинальный блок. Не обязательно возможно на всех платформах, но основные платформы 1990-х годов позволят коду определять, может ли realloc
заставить вещи двигаться, и определить, какое смещение указателя в мертвый объект было использовано, вычитая прежний базовый адрес этого объект (обратите внимание, что настройка должна быть выполнена путем вычисления смещения, связанного с каждым мертвым указателем, а затем добавления нового указателя вместо того, чтобы пытаться вычислить "разницу" между старыми и новыми указателями - то, что было бы законно fail на многих сегментированных архитектурах).
Составляют ли "гиперсовременные" компиляторы какие-либо хорошие замены для вышеуказанного, которые не ухудшат хотя бы один из размеров, скорости или удобочитаемости кода, не предлагая никаких улучшений ни в одном из других? Из того, что я могу сказать, не только 99% компиляторов на протяжении 1990-х годов делали все вышеперечисленное, но для каждого примера можно было бы написать код таким же образом почти для всех из них. Несколько компиляторов, возможно, пытались оптимизировать сдвиги влево и вправо с помощью таблицы неохраняемых прыжков, но единственный случай, когда я могу думать о том, где компилятор 1990-х годов для платформы 1990-х годов будет иметь проблемы с "очевидным" способом кодирования любой из вышеперечисленных. Если эти гиперсовременные компиляторы перестали поддерживать классические формы, что они предлагают в качестве замены?
Ответы
Ответ 1
Современный стандарт C задается таким образом, что его можно гарантировать, что он будет переносимым тогда и только тогда, когда вы напишете свой код без каких-либо ожиданий относительно базового оборудования, на котором оно будет работать, чем на C-абстрактной машине. неявно и явно описывает.
Вы все равно можете написать для конкретного компилятора, который имеет конкретное поведение на заданном уровне оптимизации для данного целевого процессора и архитектуры, но затем не ожидайте какого-либо другого компилятора (современного или иного или даже незначительного пересмотра того, который вы написал для), чтобы изо всех сил пытаться понять ваши ожидания, если ваш код нарушает условия, когда Стандарт говорит, что необоснованно ожидать какого-либо четко определенного агностического поведения реализации.
Ответ 2
К стандарту C и стандарту С++ применяются два общих принципа:
- Поведение с неподписанными номерами обычно лучше определено, чем поведение с подписанными числами.
- Рассматривайте оптимизацию как проблему качества реализации, которой она является. Это означает, что если вас беспокоит микро-оптимизация определенного внутреннего цикла, вы должны прочитать вывод сборки вашего компилятора (например, с
gcc -S
), и если вы обнаружите, что он не работает чтобы оптимизировать четко определенное поведение для соответствующих машинных инструкций, записать отчет о дефектах с издателем вашего компилятора. (Это не работает, однако, когда издатель единственного практического компилятора, ориентированного на определенную платформу, не очень интересуется оптимизацией, такой как cc65, предназначенный для MOS 6502.)
Из этих принципов вы обычно можете получить четко определенный способ достижения одного и того же результата, а затем применить принцип доверия и подтверждения к качеству сгенерированного кода. Например, создавайте функции переключения с четко определенным поведением и позволяйте оптимизатору удалять любые ненужные проверки, которые сама архитектура гарантирует.
// Performs 2 for unsigned numbers. Also works for signed
// numbers due to rule for casting between signed and unsigned
// integer types.
inline uint32_t lsl32(uint32_t ui, unsigned int b) {
if (b >= 32) return 0;
return ui << b;
}
// Performs 3 for unsigned numbers.
inline uint32_t lsr32(uint32_t ui, unsigned int b) {
if (b >= 32) return 0;
return ui >> b;
}
// Performs 3 for signed numbers.
inline int32_t asr32(int32_t si, unsigned int b) {
if (si >= 0) return lsr32(si, b);
if (b >= 31) return -1;
return ~(~(uint32)si >> b);
}
Для 4 и 5, приведение в неподписанное, выполните математику и верните ее в подписанное. Это приводит к неконтролируемому определению поведения.