У меня есть небольшой проект на C++, разработанный для Win32, и я хочу перенести его на OSX. В коде используются такие функции, как _bittest и _bittest64, но я не нашел таких функций в файлах заголовков XCode.
Что может быть альтернативой этим функциям? Может быть есть хорошие рабочие полифиллы. Проект действительно унаследован, на данный момент не требуется дополнительной производительности.
В основном дубликат Использование инструкции по сборке bts с компилятором gcc. Как объясняется в моем ответе, вам не нужна фактическая инструкция на современном x86. (Хотя компиляторы упустили оптимизацию для использования версии bt и bts с регистром-регистром, которые работают быстро как на Intel, так и на AMD.)





Символы _bittest и _bittest64 являются внутренними функциями компилятора, которые выдают Инструкции по битовому тестированию, в частности x86 bt, для проверки значения бита в индексе, начинающемся с нуля.
С операндом памяти bt имеет сумасшедшее поведение цепочки битов CISC, когда битовый индекс может выходить за пределы двойного / q-слова памяти, выбранного режимом адресации. Это медленно, и поэтому компиляторы сначала загружают операнд в регистр. Но это то, для чего предназначен встроенный MSVC. В противном случае это не обязательно должно быть внутренним.
Следующий C++ соответствует поведению версии с регистром-аргументом инструкции bt, оборачивая счетчик сдвига по ширине регистра, то есть эффективно просматривая только младшие биты. (Это соответствует внутреннему MSVC если b <32 или <64.) См. Обновленный код и комментарии для обсуждения того, как реализовать семантику MSVC, которая позволяет ему осуществлять доступ за пределами указанного long или long long.
Также помните, что long является 32-разрядным типом в x64 Windows ABI, но 64-разрядным типом в x86-64 System V ABI (который вы используете в OS X, если вы не создаете устаревший 32-разрядный код). Вы можете изменить свой код на int32_t или uint32_t, чтобы не оставлять неиспользуемые биты в каждом long, в зависимости от того, как вы его используете.
inline
unsigned char bittest(long const *a, long b)
{
auto const value{ *a };
auto const mask{ 1L << (b&31) };
auto const masked_value{ value & mask };
return unsigned char{ masked_value != 0 };
}
inline
unsigned char bittest64(long long const *a, long long b)
{
auto const value{ *a };
auto const mask{ 1LL << (b&63) };
auto const masked_value{ value & mask };
return unsigned char{ masked_value != 0 };
}
Мне неизвестны встроенные функции GCC или Clang с идентичной функциональностью. При необходимости вы можете вместо этого прибегнуть к передаче инструкций сборки из реализаций функций, но bt с операндом памяти работает медленно, поэтому обычно лучше реализовать на чистом C++ и позволить компилятору хорошо выполнять свою работу.
После обсуждения кода, генерируемого встроенными функциями, стало ясно, что ранее предложенный код замены охватывает только часть функциональности. В частности, встроенные функции позволяют индексировать биты вне памяти, занятой *a. Следующие реализации также учитывают это.
inline
unsigned char bittest(std::int32_t const *a, std::int32_t b)
{
auto const bits{ reinterpret_cast<unsigned char const*>(a) };
auto const value{ bits[b >> 3] };
auto const mask{ (unsigned char)(1 << (b & 7)) };
return (value & mask) != 0;
}
inline
unsigned char bittest64(std::int64_t const *a, std::int64_t b)
{
auto const bits{ reinterpret_cast<unsigned char const*>(a) };
auto const value{ bits[b >> 3] };
auto const mask{ (unsigned char)(1 << (b & 7)) };
return (value & mask) != 0;
}
Зачем вообще здесь нужен указатель arg? Это не асм; это зависит от компилятора, где значение находится после встраивания. Так что проще просто взять аргумент по значению. Вы все равно хотите, чтобы эти функции были встроенными, поэтому вы не получите инструкций по фактическому созданию логического возвращаемого значения в целочисленном регистре.
@PeterCordes: Эти функции предназначены для замены 1: 1, поэтому я оставил интерфейс без изменений. OP переносит код и, по-видимому, не хочет изменять вызывающий код.
Хм, я добавил &31 и &63 в счетчик сдвигов, потому что x86 действительно маскирует счетчик сдвигов. Но в портативной версии этого, вероятно, следует использовать sizeof(long)*CHAR_BIT - 1 или эквивалент C++.
Да ладно, я забыл о том, что конечным источником вопроса является внутренняя функция MSVC: P. Хм, я дважды проверю, что внутренняя функция на самом деле не для версии, предназначенной для памяти, где нам действительно нужно обрабатывать индексацию вне *a.
@PeterCordes: Переносимая версия, вероятно, должна оставить код как есть, а static_assert с фактическими целочисленными размерами. В конце концов, исходная реализация ожидает 32 и 64 значения и не подстраивается под фактические размеры, используемые языковой реализацией.
Или, возможно, использовать int64_t / int32_t? Нет, вероятно, лучше всего использовать long и long long для совместимости с псевдонимами.
@PeterCordes: К сожалению, целочисленные типы фиксированной ширины не являются обязательными в C++. Соответствующая реализация может отказаться от поставки любого из них.
Да, но более или менее безопасно предположить, что любая реализация, в которой long или int является типом со знаком с дополнением до 2 и правильной шириной, будет иметь int32_t. Это является, что мы хотим проверить (за исключением того, что, я думаю, нам не нужно требовать дополнения до 2. Вероятно, нам следует привести к uint32_t, потому что MSVC long - это 32-разрядный тип, но это 64-разрядный тип в x86-64 MacOS Так что битовая индексация все равно будет нарушена.
Я проверил, и MSVC _bittest действительно использует битовый индекс напрямую с операндом памяти. godbolt.org/g/Ev42LS. Думаю, в этом весь смысл и почему это должна быть внутренняя оптимизация, а не оптимизация глазком: это индекс может вне *a, в частности, для a[b>>3]. Вероятно, мы хотим unsigned char и не гарантируем отсутствие отступов.
@PeterCordes: Другими словами: функции замены должны быть расширены, чтобы позволить произвольную индексацию. Я не уверен, что понимаю, где здесь задействован unsigned char. Вы предлагаете поменять интерфейс? Если нет, то я не уверен, что мне даст кастинг на unsigned char.
unsigned char безопасен со строгим алиасингом (например, я предполагаю, что _bittest находится на MSVC, если MSVC вообще оптимизирует на основе строгого алиасинга). И это дает нам портативный битовый доступ к объектному представлению long. Обычно у нас этого нет, потому что long может иметь заполнение, а unsigned char - нет. Мы можем взять const long * и преобразовать его в const char * внутри функции.
@PeterCordes: Будет ли эта реализация демонстрировать поведение, идентичное внутреннему поведению _bittest?
Да, я только что проверил настоящую инструкцию, и индекс массива со знаком верен. (например, mov edx, -20*8 / bt [buf+100], edx действительно обращается к [buf+80] в 64-битном режиме, где указатели являются 64-битными, что доказывает, что знак-расширение счетчика до 64-битного является правильным, как и ваш код. И, кстати, много легче читать при оптимизации Вы исключили -Ox / -O3. godbolt.org/g/PBUbV9) О, я не проверял, что b >>3 правильный, а не b / 8. Для отрицательного значения b они округляются по-разному, и в справочном руководстве ISA указано «count DIV 32» для двойного слова, к которому обращается 32-битная версия операнда.
>> 3 правильный. Нам нужен неотрицательный остаток для индексации в выбранный char. Я дважды проверил фактическое поведение с mov edx, -10 / bt [buf+8], edx, и он установил [buf+6] на 0x40 (с 0). bts использует ту же индексацию, что и bt.
И, кстати, это должен быть арифметический сдвиг вправо. Не уверен, что int32_t гарантирует что, или он все еще определен в реализации.
Хорошее обновление. К сожалению, использование char* и разделение битового индекса на байтовые / битовые компоненты, вероятно, не оптимизирует для случая b=13 или чего-то еще. Надеюсь, эти встроенные функции будут использоваться только с массивами, которые уже находятся в памяти, а не с одной скалярной переменной, и в этом случае это может не иметь значения (много).
@PeterCordes: Я обновил ответ, включив в него код, реализующий полный набор функций встроенных функций. Я не нашел документации, которая так или иначе обращалась бы к int32_t, гарантируя арифметический сдвиг вправо. Это может показаться естественным, учитывая, что он гарантированно использует дополнение до 2, и все компиляторы также выдают инструкцию sar.
Да, я не знаю каких-либо существующих реализаций C++ на машинах с двумя дополнениями, которые предпочитают реализовывать знаковый сдвиг вправо как логический, а не арифметический. GNU C может даже гарантировать, что сдвиг вправо со знаком является арифметическим (вместе с целыми числами, являющимися дополнением до 2). Это один из тех случаев, когда мы, вероятно, должны рассмотреть этот переносимый достаточно, потому что C++ допускает так много ошибок ...
Вы пытались найти документацию по этим функциям, чтобы узнать, что они делают? Таким образом, вам будет проще искать (или, возможно, реализовать самостоятельно) замену.