Работает встроенная сборка в C для четности?

Я пытаюсь вычислить битовый паритет большого количества uint64. Под битовой четностью я имею в виду функцию, которая принимает uint64 и выдает 0, если число установленных битов четное, и 1 в противном случае.

В настоящее время я использую следующую функцию (автор @Troyseph, найденный здесь):

uint parity64(uint64 n){
  n ^= n >> 1;
  n ^= n >> 2;
  n = (n & 0x1111111111111111) * 0x1111111111111111;
  return (n >> 60) & 1;
}

На той же странице SO есть следующая подпрограмма сборки (автор @papadp):

.code

; bool CheckParity(size_t Result)
    CheckParity PROC
    mov     rax, 0
    add     rcx, 0
    jnp     jmp_over
    mov     rax, 1
jmp_over:
    ret
CheckParity ENDP

END

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

Вопрос Как я могу включить вышеуказанный (или аналогичный) код в качестве встроенной сборки в мой исходный файл C, чтобы parity64() функция работает вместо этого?

(Я использую GCC с 64-битной Ubuntu 14 на Intel Xeon Haswell)


В случае, если это поможет, parity64() Функция вызывается внутри следующей процедуры:

uint bindot(uint64* a, uint64* b, uint64 entries){
    uint parity = 0;

    for(uint i=0; i<entries; ++i)
      parity ^= parity64(a[i] & b[i]);  // Running sum!

    return parity;
}

(Предполагается, что это "точечное произведение" двух векторов над полем Z/2Z, он же GF(2).)

4 ответа

Решение

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

Вашу функцию parity64 можно изменить следующим образом:

uint parity64(uint64 n){
    uint result = 0;
    __asm__("addq $0, %0" : : "r"(n)  :);
    __asm__("jnp 1f");
    __asm__("movl $1, %0" : "=r"(result) : : );
    __asm__("1:");
    return result;
}

Но, как прокомментировал @MichaelPetch, флаг четности вычисляется только на младших 8 битах. Так что это будет работать для вас, если ваш n меньше 255. Для больших чисел вам придется использовать код, который вы указали в своем вопросе.

Чтобы заставить его работать на 64 бита, вы можете свернуть четность 32-битного целого в один байт, выполнив

n = (n >> 32) ^ n;
n = (n >> 16) ^ n;
n = (n >> 8) ^ n;

Этот код должен быть только в начале функции перед сборкой.

Вам придется проверить, как это влияет на производительность.

Наиболее оптимизированный я мог бы получить это

uint parity64(uint64 n){
    unsigned char result = 0;
    n = (n >> 32) ^ n;
    n = (n >> 16) ^ n;
    n = (n >> 8) ^ n;
    __asm__("test %1, %1 \n\t"
            "setp %0"
            : "+r"(result)
            : "r"(n)
            :
    );
    return result;
}

Это может показаться немного резким, но я считаю, что это нужно сказать. Пожалуйста, не принимайте это на свой счет; Я не имею в виду это как оскорбление, тем более что вы уже признали, что "знаете, что рядом нет собрания". Но если вы думаете, код, как это:

CheckParity PROC
    mov     rax, 0
    add     rcx, 0
    jnp     jmp_over
    mov     rax, 1
 jmp_over:
    ret
CheckParity ENDP

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

   xor     eax, eax
   test    ecx, ecx   ; logically, should use RCX, but see below for behavior of PF
   jnp     jmp_over
   mov     eax, 1     ; or possibly even "inc eax"; would need to verify
jmp_over:
   ret

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

xor     eax, eax
test    ecx, ecx
setp    al
ret

Или, возможно, эквивалент (который будет быстрее на некоторых процессорах, но не обязательно на всех):

xor     eax, eax
test    ecx, ecx
mov     ecx, 1
cmovp   eax, ecx
ret

И это только те улучшения, которые я мог видеть вне головы, учитывая мои существующие знания ISA для x86 и предыдущие тесты, которые я провел. Но, чтобы никого не обмануть, это, несомненно, не самый быстрый код, потому что (заимствуя у Майкла Абраша), "не существует такого понятия, как самый быстрый код" - кто-то может практически всегда сделать его еще быстрее.

Существует достаточно проблем с использованием встроенной сборки, когда вы опытный программист на языке ассемблера и мастер, когда дело доходит до тонкостей ISA x86. Оптимизаторы чертовски хороши в наши дни, а это значит, что настоящему гуру достаточно сложно создать лучший код (хотя, конечно, не невозможно). Также требуются надежные тесты, которые подтвердят ваши предположения и подтвердят, что оптимизированная встроенная сборка действительно быстрее. Никогда не используйте встроенную сборку, чтобы перехитрить оптимизатор компилятора, не выполняя хороший тест. Я не вижу никаких доказательств в вашем вопросе, что вы сделали что-то подобное. Я размышляю здесь, но похоже, что вы видели, что код был написан на ассемблере, и предполагали, что это будет быстрее. Это редко бывает. Компиляторы C, в конечном счете, также генерируют код на ассемблере, и он часто является более оптимальным, чем то, что мы, люди, способны производить, учитывая ограниченное количество времени и ресурсов, гораздо меньший опыт.

В этом конкретном случае существует понятие, что встроенная сборка будет быстрее, чем выходные данные компилятора C, поскольку компилятор C не сможет интеллектуально использовать встроенный флаг четности архитектуры P86 (PF) в свою пользу. И вы можете быть правы, но это довольно шаткое предположение, далеко не универсальное. Как я уже сказал, оптимизирующие компиляторы в наше время довольно умны, и они оптимизируют под конкретную архитектуру (при условии, что вы укажете правильные параметры), поэтому меня совсем не удивит, что оптимизатор будет генерировать код, использующий PF. Вы должны посмотреть на разборки, чтобы убедиться наверняка.

В качестве примера того, что я имею в виду, рассмотрим BSWAP инструкция, которую предоставляет x86. Вы можете наивно полагать, что встроенная сборка потребуется, чтобы воспользоваться этим, но это не так. Следующий код C компилируется в BSWAP Инструкция практически по всем основным компиляторам:

uint32 SwapBytes(uint32 x)
{
    return ((x << 24) & 0xff000000 ) |
           ((x <<  8) & 0x00ff0000 ) |
           ((x >>  8) & 0x0000ff00 ) |
           ((x >> 24) & 0x000000ff );
}

Производительность будет эквивалентна, если не лучше, потому что оптимизатор лучше знает, что делает код. Фактически, главное преимущество этой формы перед встроенной сборкой заключается в том, что компилятор может выполнять константное свертывание с этим кодом (т. Е. При вызове с константой времени компиляции). Кроме того, код более читабелен (по крайней мере, для программиста на C), гораздо менее подвержен ошибкам и значительно проще в обслуживании, чем если бы вы использовали встроенную сборку. О, и я упоминал, что он достаточно переносимый, если вы когда-нибудь хотели использовать архитектуру, отличную от x86?

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

  • Является ли этот код узким местом в моем приложении, так что его оптимизация могла бы хоть как-то ощутимо измениться?
  • Оптимизатор действительно выдает неоптимальные инструкции машинного языка для кода, который я написал?
  • Я ошибаюсь в том, что наивно считаю неоптимальным? Возможно, оптимизатор знает больше, чем я, о целевой архитектуре, и то, что выглядит как медленный или неоптимальный код, на самом деле быстрее. (Помните, что меньше кода не обязательно быстрее.)
  • Испытал ли я его в значимом, реальном тесте и доказал, что сгенерированный компилятором код работает медленно и что моя встроенная сборка на самом деле быстрее?
  • Нет ли абсолютно никакого способа, которым я мог бы настроить код C, чтобы убедить оптимизатор выдавать лучший машинный код, близкий, равный или даже превосходящий производительность моей встроенной сборки?

В попытке ответить на некоторые из этих вопросов я установил небольшой тест. (Использование MSVC, потому что это то, что мне удобно; если вы ориентируетесь на GCC, лучше использовать этот компилятор, но мы все еще можем получить общее представление. Я использую и рекомендую библиотеку бенчмаркинга Google.) И я сразу столкнулся с проблемами. Понимаете, я сначала запускаю свои тесты в режиме "отладки", скомпилированные утверждения, которые проверяют, что мой "оптимизированный"/"оптимизированный" код фактически дает те же результаты для всех тестовых случаев, что и исходный код (предположительно известно, что работает / правильно). В этом случае утверждение сразу срабатывает. Оказывается, что CheckParity рутина, написанная на ассемблере, не возвращает идентичные результаты parity64 рутина написана на C! Ой-ой. Ну, это еще одна пуля, которую мы должны добавить в приведенный выше список:

  • Я гарантировал, что мой "оптимизированный" код возвращает правильные результаты?

Это особенно важно, потому что легко сделать что-то быстрее, если вы тоже ошибаетесь.:-) Я шучу, но не полностью, потому что я делал это много раз в поисках более быстрого кода.

Я полагаю, что Майкл Петч уже указал причину несоответствия: в реализации x86 флаг четности (PF) касается только битов младшего байта, а не всего значения. Если это все, что вам нужно, то отлично. Но даже тогда мы можем вернуться к коду C и дополнительно оптимизировать его, чтобы выполнять меньше работы, что сделает его быстрее - возможно, быстрее, чем код сборки, устраняя одно преимущество, которое когда-либо было у встроенной сборки.

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

... кроме, не совсем. Когда я впервые составил этот ответ, мой тест показал, что черновой вариант 9 его "подправленного" кода все еще не дает того же результата, что и исходная функция C, поэтому он не подходит для наших тестов. В комментарии вы говорите, что его код "работает" для вас, что означает, что либо (A) исходный код C выполнял дополнительную работу, делая его излишне медленным, а это означает, что вы, вероятно, можете настроить его, чтобы превзойти встроенную сборку в собственной игре или, что еще хуже, (B) у вас недостаточно тестов, и новый "оптимизированный" код на самом деле является ошибкой, ожидающей ожидания. С тех пор Ped7g предложил несколько исправлений, которые исправили ошибку, приводившую к возвращению неверного результата, и дополнительно улучшили код. Количество ввода, необходимое здесь, и количество черновиков, которые он прошел, должны служить свидетельством сложности написания правильной встроенной сборки, чтобы превзойти компилятор. Но мы еще даже не закончили! Его встроенная сборка остается неправильно написанной. SETcc инструкции требуют 8-битный регистр в качестве своего операнда, но его код не использует спецификатор регистра, чтобы запросить это, что означает, что код либо не скомпилируется (потому что Clang достаточно умен, чтобы обнаружить эту ошибку), либо скомпилируется в GCC но не будет выполняться должным образом, потому что эта инструкция имеет недопустимый операнд.

Я уже убедил вас в важности тестирования? Я возьму это на веру и перейду к тестированию. Результаты теста используют окончательный вариант кода Ajay с улучшениями Ped7g и моими дополнительными настройками. Я также сравниваю некоторые другие решения из того вопроса, который вы связали, модифицированный для 64-битных целых чисел, плюс пару моих собственных изобретений. Вот мои результаты тестов (мобильный Haswell i7-4850HQ):

Benchmark                         Time          CPU      Iterations
-------------------------------------------------------------------
Naive                            36 ns         36 ns       19478261
OriginalCCode                     4 ns          4 ns      194782609
Ajay_Brahmakshatriya_Tweaked      4 ns          4 ns      194782609
Shreyas_Shivalkar                37 ns         37 ns       17920000
TypeIA                            5 ns          5 ns      154482759
TypeIA_Tweaked                    4 ns          4 ns      160000000
has_even_parity                 227 ns        229 ns        3200000
has_even_parity_Tweaked          36 ns         36 ns       19478261
GCC_builtin_parityll              4 ns          4 ns      186666667
PopCount                          3 ns          3 ns      248888889
PopCount_Downlevel                5 ns          5 ns      100000000

Теперь имейте в виду, что они предназначены для случайно сгенерированных 64-битных входных значений, что нарушает предсказание ветвлений. Если ваши входные значения смещены предсказуемым образом, в сторону четности или не четности, тогда предсказатель ветвления будет работать для вас, а не против вас, и некоторые подходы могут быть быстрее. Это подчеркивает важность сравнительного анализа данных, имитирующих реальные случаи использования. (Тем не менее, когда я пишу общие библиотечные функции, я склонен оптимизировать случайные входы, балансировать размер и скорость.)

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

Naive Функция - это совершенно неоптимизированная проверка работоспособности для определения паритета, взятого отсюда. Я использовал его для проверки даже вашего исходного кода на C, а также для обеспечения базовых показателей для тестов. Поскольку он проходит один за другим каждый бит, он относительно медленный, как и ожидалось:

unsigned int Naive(uint64 n)
{
   bool parity = false;
   while (n)
   {
      parity = !parity;
      n &= (n - 1);
   }
   return parity;
}

OriginalCCode это именно то, на что это похоже - это исходный код C, который у вас был, как показано в вопросе. Обратите внимание, что он публикуется точно так же, как и исправленная / исправленная версия встроенного ассемблерного кода Аджая Брахмакшатрии! Теперь, поскольку я запустил этот тест в MSVC, который не поддерживает встроенную сборку для 64-разрядных сборок, мне пришлось использовать модуль внешней сборки, содержащий функцию, и вызывать его оттуда, что привело к дополнительным издержкам. При встроенной сборке GCC компилятор, вероятно, смог бы встроить код, тем самым исключив вызов функции. Так что в GCC вы можете увидеть версию встроенной сборки на наносекунду быстрее (а может и нет). Это того стоит? Ты будешь судьей. Для справки, это код, который я тестировал для Ajay_Brahmakshatriya_Tweaked:

Ajay_Brahmakshatriya_Tweaked PROC
    mov    rax, rcx   ; Windows 64-bit calling convention passes parameter in ECX (System V uses EDI)
    shr    rax, 32
    xor    rcx, rax
    mov    rax, rcx
    shr    rax, 16
    xor    rcx, rax
    mov    rax, rcx
    shr    rax, 8
    xor    eax, ecx   ; Ped7g's TEST is redundant; XOR already sets PF
    setnp  al
    movzx  eax, al
    ret
Ajay_Brahmakshatriya_Tweaked ENDP

Функция с именем Shreyas_Shivalkar от его ответа здесь, который является просто вариацией на тему "проход по каждому биту", и, в соответствии с ожиданиями, медленный:

Shreyas_Shivalkar PROC
   ; unsigned int parity = 0;
   ; while (x != 0)
   ; {
   ;    parity ^= x;
   ;    x     >>= 1;
   ; }
   ; return (parity & 0x1);
   xor     eax, eax
   test    rcx, rcx
   je      SHORT Finished
Process:
   xor     eax, ecx
   shr     rcx, 1
   jne     SHORT Process
Finished:
   and     eax, 1
   ret
Shreyas_Shivalkar ENDP

TypeIA а также TypeIA_Tweaked код из этого ответа, модифицированный для поддержки 64-битных значений, и моя измененная версия. Они распараллеливают операцию, что приводит к значительному повышению скорости по сравнению со стратегией "проход по каждому биту". "Подправленная" версия основана на оптимизации, изначально предложенной Мэтью Хендри для Шона Эрона Андерсона Bit Twiddling Hacks, и дает нам небольшое ускорение по сравнению с оригиналом.

unsigned int TypeIA(uint64 n)
{
   n ^= n >> 32;
   n ^= n >> 16;
   n ^= n >> 8;
   n ^= n >> 4;
   n ^= n >> 2;
   n ^= n >> 1;
   return !((~n) & 1);
}

unsigned int TypeIA_Tweaked(uint64 n)
{
   n ^= n >> 32;
   n ^= n >> 16;
   n ^= n >> 8;
   n ^= n >> 4;
   n &= 0xf;
   return ((0x6996 >> n) & 1);
}

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

unsigned int has_even_parity(uint64 n)
{
   uint64 count = 0;
   uint64 b     = 1;
   for (uint64 i = 0; i < 64; ++i)
   {
      if (n & (b << i)) { ++count; }
   }
   return (count % 2);
}

has_even_parity_Tweaked является альтернативной версией вышеупомянутого, которая сохраняет ветвь, используя преимущество того факта, что булевы значения неявно преобразуются в 0 и 1. Это значительно быстрее, чем оригинал, синхронизируя время, сравнимое с "наивным" подходом:

unsigned int has_even_parity_Tweaked(uint64 n)
{
   uint64 count = 0;
   uint64 b     = 1;
   for (uint64 i = 0; i < 64; ++i)
   {
      count += static_cast<int>(static_cast<bool>(n & (b << i)));
   }
   return (count % 2);
}

Теперь мы входим в хорошие вещи. Функция GCC_builtin_parityll состоит из ассемблерного кода, который GCC будет выдавать, если вы используете его __builtin_parityll внутренняя. Несколько других предположили, что вы используете это, и я должен поддержать их. Его производительность находится на одном уровне с лучшими, которые мы видели до сих пор, и у него есть пара дополнительных преимуществ: (1) он делает код простым и читаемым (проще, чем версия C); (2) он переносим на разные архитектуры, и можно ожидать, что он там тоже будет быстрым; (3) поскольку GCC улучшает свою реализацию, ваш код может стать быстрее с простой перекомпиляцией. Вы получаете все преимущества встроенной сборки без каких-либо недостатков.

GCC_builtin_parityll PROC     ; GCC's __builtin_parityll
    mov    edx, ecx
    shr    rcx, 32
    xor    edx, ecx
    mov    eax, edx
    shr    edx, 16
    xor    eax, edx
    xor    al, ah
    setnp  al
    movzx  eax, al
    ret
GCC_builtin_parityll ENDP

PopCount оптимизированная реализация моего собственного изобретения. Чтобы придумать это, я вернулся и подумал, что мы на самом деле пытаемся сделать. Определение "четность" - это четное число установленных бит. Поэтому его можно рассчитать, просто посчитав количество установленных битов и проверив, является ли этот счет четным или нечетным. Это две логические операции. По счастливой случайности, в последних поколениях процессоров x86 (Intel Nehalem или AMD Barcelona и более новых) есть инструкция, которая подсчитывает количество установленных битов: POPCNT (количество населения, или вес Хэмминга) - который позволяет нам писать ассемблерный код, который делает это за две операции.

(Хорошо, на самом деле три инструкции, потому что есть ошибка в реализации POPCNT на некоторых микроархитектурах, которые создают ложную зависимость от своего регистра назначения, и чтобы гарантировать максимальную пропускную способность кода, нам нужно сломать эту зависимость, предварительно очистив регистр назначения. К счастью, это очень дешевая операция, которую обычно можно обработать "бесплатно" путем переименования регистра.)

PopCount PROC
    xor     eax, eax   ; break false dependency
    popcnt  rax, rcx
    and     eax, 1
    ret
PopCount ENDP

На самом деле, как выясняется, GCC знает, что испустить именно этот код для __builtin_parityll присущий микроархитектуре, которая поддерживает POPCNT, В противном случае используется резервная реализация, показанная выше. Как видно из тестов, это самый быстрый код. Это не является существенным отличием, поэтому вряд ли это имеет значение, если вы не будете делать это многократно в узком цикле, но это измеримая разница, и, вероятно, вы не будете оптимизировать это так сильно, если ваш профилировщик не сообщит, что это горячее пятно.

Но POPCNT Недостатком инструкций является то, что они недоступны на старых процессорах, поэтому я также измерил "запасную" версию кода, которая выполняет подсчет населения с помощью последовательности универсально поддерживаемых инструкций. Это PopCount_Downlevel Функция, взятая из моей личной библиотеки, изначально адаптирована из этого ответа и других источников.

PopCount_Downlevel PROC
    mov     rax, rcx
    shr     rax, 1
    mov     rdx, 5555555555555555h
    and     rax, rdx
    sub     rcx, rax
    mov     rax, 3333333333333333h
    mov     rdx, rcx
    and     rcx, rax
    shr     rdx, 2
    and     rdx, rax
    add     rdx, rcx
    mov     rcx, 0FF0F0F0F0F0F0F0Fh
    mov     rax, rdx
    shr     rax, 4
    add     rax, rdx
    mov     rdx, 0FF01010101010101h
    and     rax, rcx
    imul    rax, rdx
    shr     rax, 56
    and     eax, 1
    ret
PopCount_Downlevel ENDP

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

unsigned int PopCount_Downlevel(uint64 n)
{
    uint64 temp = n - ((n >> 1) & 0x5555555555555555ULL);
    temp        = (temp & 0x3333333333333333ULL) + ((temp >> 2) & 0x3333333333333333ULL);
    temp        = (temp + (temp >> 4)) & 0x0F0F0F0F0F0F0F0FULL;
    temp        = (temp * 0x0101010101010101ULL) >> 56;
    return (temp & 1);
}

Но запустите свои собственные тесты, чтобы увидеть, не лучше ли вам использовать одну из других реализаций, например OriginalCCode, что упрощает работу и, следовательно, требует меньше общих инструкций. Интересный факт: компилятор Intel (ICC) всегда использует алгоритм на основе подсчета населения для реализации __builtin_parityll; он излучает POPCNT инструкция, если целевая архитектура поддерживает это, или иначе, она моделирует это, используя по существу тот же самый код, который я показал здесь.

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

Поскольку C отстой при обработке битовых операций, я предлагаю использовать встроенные функции gcc, в данном случае __builtin_parityl(). Увидеть:

https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html

Как я могу включить вышеуказанный (или аналогичный) код в качестве встроенной сборки в мой исходный файл C, чтобы parity64() функция работает вместо этого?

Это проблема XY... Вы думаете, что вам нужно встроить эту сборку, чтобы воспользоваться ее преимуществами, поэтому вы спросили о том, как встроить ее... но вам не нужно вставлять ее.

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

В parity64.c у вас должна быть портативная версия (с именем оболочки bool CheckParity(size_t result)), который вы можете использовать по умолчанию в не x86/64 ситуациях.

Вы можете скомпилировать это в объектный файл следующим образом:gcc -c parity64.c -o parity64.o

... и затем связать объектный код, сгенерированный из сборки, с кодом C:gcc bindot.c parity64.o -o bindot


В parity64_x86.s у вас может быть следующий код сборки из вашего вопроса:

.code

; bool CheckParity(size_t Result)
    CheckParity PROC
    mov     rax, 0
    add     rcx, 0
    jnp     jmp_over
    mov     rax, 1
jmp_over:
    ret
CheckParity ENDP

END

Вы можете скомпилировать это в альтернативу parity64.o объектный файл объектный код с использованием gcc с помощью этой команды: gcc -c parity64_x86.s -o parity64.o

... а затем связать объектный код, сгенерированный следующим образом: gcc bindot.c parity64.o -o bindot


Точно так же, если вы хотите использовать __builtin_parityl вместо этого (как следует из ответа hdantes, вы можете (и должны) еще раз сохранить этот код отдельно (в том же месте, где вы храните другие оптимизации gcc/x86) от вашего переносимого кода. parity64_x86.c Ты можешь иметь:

bool CheckParity(size_t result) {
    return __builtin_parityl(result);
}

Чтобы скомпилировать это, ваша команда будет: gcc -c parity64_x86.c -o parity64.o

... а затем связать объектный код, сгенерированный следующим образом: gcc bindot.c parity64.o -o bindot

На заметку, если вы хотите проверить сборку gcc будет производить из этого: gcc -S parity64_x86.c


Комментарии в вашей сборке показывают, что эквивалентный прототип функции в C будет bool CheckParity(size_t Result)с учетом этого вот что bindot.c может выглядеть так:

extern bool CheckParity(size_t Result);

uint64_t bindot(uint64_t *a, uint64_t *b, size_t entries){
    uint64_t parity = 0;

    for(size_t i = 0; i < entries; ++i)
        parity ^= a[i] & b[i];  // Running sum!

    return CheckParity(parity);
}

Вы можете построить это и связать это с любым из вышеупомянутых parity64.o версии вроде так: gcc bindot.c parity64.o -o bindot...

Я настоятельно рекомендую прочитать руководство для вашего компилятора, когда у вас есть время...

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