Как я могу безопасно усреднить два целых числа без знака в С++?

Используя только целочисленную математику, я хотел бы «безопасно» усреднить два целых числа без знака в С++.

Под «безопасным» я подразумеваю предотвращение переполнения (и всего остального, о чем можно подумать).

Например, легко усреднить 200 и 5000:

unsigned int a = 200;
unsigned int b = 5000;
unsigned int average = (a + b) / 2; // Equals: 2600 as intended

Но в случае 4294967295 и 5000 тогда:

unsigned int a = 4294967295;
unsigned int b = 5000;
unsigned int average = (a + b) / 2; // Equals: 2499 instead of 2147486147

Лучшее, что я придумал, это:

unsigned int a = 4294967295;
unsigned int b = 5000;
unsigned int average = (a / 2) + (b / 2); // Equals: 2147486147 as expected

Есть ли лучшие способы?


person Tim    schedule 28.09.2010    source источник
comment
Третий вариант даст неправильный ответ, если и a, и b нечетны (поскольку обе половины будут округлены в меньшую сторону).   -  person IVlad    schedule 28.09.2010
comment
Патент США № 6 007 232. Вычисление среднего значения двух целых чисел, округленных до нуля, за один цикл инструкций: google.com/ patches?id=eAIYAAAAEBAJ&dq=6007232 в основном использует _1_   -  person Jackson Pope    schedule 28.09.2010
comment
...Вот это да. Я приберегу эту ссылку, чтобы в следующий раз кто-нибудь пожаловался на патенты на программы.   -  person Arun    schedule 29.09.2010
comment
интересно, сколько ответов ниже содержат это запатентованное решение. Я уверен, что большинство/все они разработали его независимо, возможно, даже на месте для их ответа. Казалось бы, это указывает на то, что патент не соответствует стандарту неочевидности.   -  person Stephen Canon    schedule 29.09.2010
comment
это патент на оборудование (обратите внимание, что результат получается за один такт)   -  person rmeador    schedule 29.09.2010
comment
@ pm100: я не уверен, что это действительно различие. Код, написанный @ArunSaha, сделает ЦП схемой, описанной в патенте. Это может даже работать в одном цикле инструкций на современном x86, но я не уверен. Несмотря на это, этот код C++ можно было бы тривиально преобразовать в код VHDL, и тогда он является аппаратным...   -  person pm100    schedule 29.09.2010
comment
@Martin York: Вы говорите, что ответ оператора не работает? он знает. Если вы говорите о комментарии ArunSaha или ответе на sellibitze, то вы забыли часть _1_.   -  person rmeador    schedule 29.09.2010
comment
Круто, это именно то внимание, которое я искал.   -  person    schedule 29.09.2010


Ответы (10)


Ваш последний подход кажется многообещающим. Вы можете улучшить это, вручную рассматривая младшие биты a и b:

unsigned int average = (a / 2) + (b / 2) + (a & b & 1);

Это дает правильные результаты, если и a, и b нечетны.

person sellibitze    schedule 28.09.2010
comment
Говоря о патентах на программное обеспечение, кажется, что патентная заявка: 20090249356 пытается запатентовать то, что хорошо известно в компьютерной индустрии. Круговые очереди с одним производителем и одним потребителем без CAS известны уже почти 30 лет. (Я написал свой первый в начале 80-х) Я написал, чтобы пожаловаться, но они сказали, что уже слишком поздно. Я думаю, что патентное бюро должно быть завалено техническими электронными письмами ненависти по этому поводу. - person Tim; 28.09.2010
comment
Есть небольшая проблема с его использованием: у Samsung есть на него патент. google.com/patents?id=eAIYAAAAAEBAJ&dq=6007232 - person nbourbaki; 29.09.2010
comment
Работает только для положительных целых чисел, так как последняя часть игнорирует бит знака. - person folone; 04.10.2010
comment
мне это нравится, но что, если есть ошибка из-за целочисленного деления? - person crazypeter; 23.02.2019

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

unsigned int average = low + ((high - low) / 2);

Вот соответствующая статья: http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html

person Sheldon L. Cooper    schedule 28.09.2010
comment
Почему бы не быть? Вы никогда не делите на 0, что является единственным целочисленным делением, которое приведет к ошибке. - person ianmac45; 28.09.2010
comment
Это классический ответ на эту проблему, особенно когда вы уже знаете, какое значение является высоким, а какое низким — например, выбрав среднюю точку. - person cHao; 28.09.2010
comment
заказывать их будет слишком дорого - person Mark Ransom; 28.09.2010
comment
@ruslik: если вы не знаете порядок априори, как в статье по ссылке (что, вероятно, является наиболее распространенным вариантом использования целочисленного усреднения). - person ruslik; 29.09.2010
comment
@Стивен, не совсем так. хотя технически это ошибка, в бинарном поиске это практически невозможно. Я уверен, что в этом конкретном случае сумма переполнялась довольно часто. - person Stephen Canon; 29.09.2010
comment
@ruslik, не совсем. Может быть, десять лет назад это было почти невозможно, но не сейчас. - person ruslik; 29.09.2010
comment
@ArunSaha: неправильно! исходная проблема была о переполнении. в этом случае вы разрешаете _1_ быть подписанным, так что это может легко переполниться так же, как и в исходной задаче. вы можете избежать этого, только считая эту разницу беззнаковой, поэтому вы должны знать, какая из них больше. - person Sheldon L. Cooper; 29.09.2010
comment
@Sheldon: снова неправильно :) на большинстве машин размер high - low по умолчанию равен размеру указателя, поэтому вам нужна специальная машина для такого переполнения, с огромным адресным пространством и маленькими целыми числами. - person ruslik; 29.09.2010
comment
@ruslik: нет, вы имели в виду код Java, в котором размер int по-прежнему будет 32 бита. Пожалуйста, внимательно прочитайте статью, прежде чем делать о ней резкие замечания. - person ruslik; 29.09.2010
comment
Это очень эффективно, если вы уже знаете, какой из ассемблерных инструкций выше, меньше или быстрее, чем верхний ответ. Особенно на не-ARM, где стоимость правого сдвига не бесплатна как часть других инструкций. godbolt.org/g/bSZHdE имеет выходные данные asm для x86 и ARM для обеих версий. - person Sheldon L. Cooper; 29.09.2010
comment
Вам нужно добавить скобки вокруг ваших смен; в противном случае вы получите: _1_. (Однако ваш второй пример верен). - person Peter Cordes; 11.04.2018

Ваш метод неверен, если оба числа нечетные, например 5 и 7, среднее значение равно 6, но ваш метод № 3 возвращает 5.

Попробуй это:

average = (a>>1) + (b>>1) + (a & b & 1)

только с математическими операторами:

average = a/2 + b/2 + (a%2) * (b%2)
person iniju    schedule 28.09.2010
comment
@alxx: любой разумный компилятор все равно оптимизирует деление на два в сдвиг. - person Stephen Canon; 28.09.2010
comment
у samsung есть патент и на второй тоже? - person Stephen Canon; 29.09.2010
comment
+1: я никогда не писал встроенный ассемблер :(, не могли бы вы прокомментировать и объяснить три строки, особенно то, как выбираются значения _1_ и _2_. - person subsub; 27.04.2012

Если вы не возражаете против небольшого встроенного ассемблера x86 (синтаксис GNU C), вы можете воспользоваться предложением supercat использовать rotate-with-carry после добавления, чтобы поместить старшие 32 бита полного 33-битного результата в регистр.

Конечно, вы обычно должны возражать против использования inline-asm, потому что это нарушает некоторые оптимизации (https://gcc.gnu.org/wiki/DontUseInlineAsm). Но вот все равно:

// works for 64-bit long as well on x86-64, and doesn't depend on calling convention
unsigned average(unsigned x, unsigned y)
{
    unsigned result;
    asm("add   %[x], %[res]\n\t"
        "rcr   %[res]"
        : [res] "=r" (result)   // output
        : [y] "%0"(y),  // input: in the same reg as results output.  Commutative with next operand
          [x] "rme"(x)  // input: reg, mem, or immediate
        :               // no clobbers.  ("cc" is implicit on x86)
    );
    return result;
}

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

RCR-by-one не слишком медленный, но на Skylake это все еще 3 мкп с задержкой в ​​2 цикла. Это отлично работает на процессорах AMD, где RCR имеет задержку в один цикл. (Источник: таблицы инструкций Agner Fog, см. также x86 пометить вики для ссылок на производительность x86). Это все еще лучше, чем версия @sellibitze, но хуже, чем версия @Sheldon, зависящая от порядка. (См. код на Godbolt)


Но помните, что встроенный ассемблер отменяет такие оптимизации, как распространение констант, поэтому в этом случае любая версия на чистом C++ будет лучше.

И правильный ответ...

person fredoverflow    schedule 28.09.2010
comment
В начале встроенной сборки в стеке есть четыре 4-байтовых значения, начиная с EBP: EBP+0 (предыдущий EBP, перед вызовом функции), EBP+4 (предыдущий счетчик команд EIP), EBP+ 8 (х) и EBP+12 (у). Ожидается, что функция поместит свой результат в EAX, поэтому сборка начинается с перемещения x туда. Затем он добавляет y, и переполнение этой операции установит бит переноса (отсутствие переполнения очистит бит). RCR — это поворот вправо с переносом, который сдвигает EAX на один бит вправо (делит его на два) и сдвигает бит переноса в самый старший бит EAX. - person Arun; 29.09.2010
comment
Ссылка: cse.nd.edu/~dthain/ курсы/cse40243/fall2008/ia32-intro.html (в разделе «Определение функций»). Кроме того, используется соглашение о вызовах _1_ (по умолчанию для функций C и C++, не являющихся членами), которое вы можете просмотреть, если хотите получить дополнительную информацию. - person Josh Townzen; 29.09.2010
comment
Добавление оставляет установленным бит переноса, когда происходит переполнение (и для хранения результата требуется еще один бит). Затем вы выполняете цикл переноса вправо (делая eax и флаг переноса фактически 33-битным регистром), который фактически делится на 2. Затем вы отбрасываете флаг переноса (который теперь содержит исходный младший значащий бит из eax) и возвращаете eax в качестве результата. Блестящий. - person Josh Townzen; 29.09.2010
comment
@Josh @Tomek: в беззнаковой арифметике нет такой вещи, как переполнение, это называется перенос (отсюда и название флаг переноса) . - person Tomek; 29.09.2010
comment
Это недопустимая встроенная сборка, поскольку она не кодирует зависимость операнда. Компилятор может оптимизировать его или получить доступ к фиктивным данным, когда функция будет встроена. - person fredoverflow; 29.09.2010
comment
Вы не можете просто написать базовый ассемблер GNU C внутри функции и оставить значение в %eax. Что касается компилятора, вы только что написали функцию, которая достигает конца непустой функции, не возвращая значения. Это не удается, как только вы включаете оптимизацию, а может быть, и раньше. Всегда используйте расширенный синтаксис asm с входными и выходными операндами. (См. вики тегов встроенной сборки). И, как говорит Р., конечно, все три ассемблерные инструкции должны быть частью одного и того же оператора ассемблера. - person R.. GitHub STOP HELPING ICE; 07.09.2011
comment
@PeterCordes Пожалуйста, не стесняйтесь улучшить мой ответ, благословляю вас! - person Peter Cordes; 30.11.2016
comment
@RossRidge: xD, надо было прокомментировать, что я работаю над редактированием. Мой был почти готов, когда вы написали. И \@fredoverflow: вот вам версия, максимально свободная от хлама для встроенного ассемблера. Хотя в целом я бы все равно не рекомендовал. Обычно лучше, если компилятор понимает, что происходит, чтобы он мог больше узнать о значениях переменных. - person fredoverflow; 30.11.2016
comment
@PeterCordes Ну, в целом я думаю, что ваше редактирование лучше, но для справки мое редактирование дает пример того, как вы можете использовать здесь коммутативный модификатор. - person Peter Cordes; 30.11.2016
comment
@PeterCordes Кроме того, выходной операнд не является ранним затиранием, так как все входные операнды потребляются в начале. (например, _1_ будет допустимым). - person Ross Ridge; 30.11.2016
comment
@RossRidge: хорошие моменты. Я попробовал вашу коммутативную идею, но она не меняет asm. Вероятно, он не может этого сделать из-за ограничения соответствия с выходным операндом. Во всяком случае, обновился. - person Ross Ridge; 30.11.2016
comment
@PeterCordes Поместите строку типа _1_ перед оператором asm, скомпилируйте для 32-битной системы и оптимизируйте с помощью -O3, и вы должны увидеть, что она использует _2_ уже в EAX в качестве операнда _3_/_4_. - person Peter Cordes; 30.11.2016
comment
@PeterCordes И, очевидно, вам также нужно использовать GCC 4.8. Не знаю, почему это сломалось в более поздних компиляторах. - person Ross Ridge; 30.11.2016
comment
@RossRidge: аааа, я как раз собирался попросить вас взглянуть на godbolt.org/g/WPNxLB (gcc6.2 и clang3.9), так как я хотел спать и понял, что что-то упускаю. Но да, gcc4.8.5 (godbolt.org/g/QJhyI6) действительно выигрывает от %. Но еще хуже без него: две лишних гостиницы вместо одной. - person Ross Ridge; 30.11.2016
comment
Разве у этого нет патентных проблем, как указано выше? - person Peter Cordes; 30.11.2016

То, что у вас есть, прекрасно, с небольшой деталью, что оно будет утверждать, что среднее значение 3 и 3 равно 2. Я предполагаю, что вы этого не хотите; к счастью, есть простое решение:

(A&B)+((A^B)>>1)
person Jonathan Olson    schedule 14.02.2013
comment
Недавний вопрос asm 8051 (Среднее из 2 регистров, избегая переполнения в сборке?) показывает этот метод. Когда я ответил, я не понял, что в этом вопросе и ответе были ответы с ротацией через перенос (включая этот или ответ на встроенный ассемблер x86 asm), иначе я бы оставил его закрытым как дубликат. - person IdeaHat; 15.01.2014

Это просто увеличивает среднее значение в случае, если оба деления были усечены.

unsigned int average = a/2 + b/2 + (a & b & 1);

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

person Stephen Canon    schedule 28.09.2010

Между прочим, на машинах с меньшим размером слова различия могут быть еще более существенными. В 8-битной серии PIC-18 для усреднения двух 32-битных чисел потребовалось бы двенадцать инструкций. Выполнение сдвигов, добавления и исправления потребовало бы 5 инструкций для каждого смещения, восьми для добавления и восьми для исправления, то есть 26 (разница не совсем в 2,5 раза, но, вероятно, более значительная в абсолютном выражении).

В C++20 вы можете использовать _1_:

person supercat    schedule 28.09.2010
comment
Это неправильно, потому что может быть переполнение. Рассмотрим 8-битные целые числа, и вы хотите найти среднее значение 0xff и 0x01. Должно быть 0x80, верно? Вычисление: 0xff&0x01=0x01, 0x01‹‹1=0x02, 0xff^0x01=0xfe, 0x02+0xfe=0x00 (поскольку int 8-битные, 1 в 0x02+0xfe=0x100 теряется), 0x00››1=0x00 . 0x00!=0x80. - person Peter Cordes; 13.03.2021

Документ P0811R3, в котором представлен std::midpoint рекомендовал этот фрагмент (слегка адаптированный для работы с С++ 11):

template <class T>
constexpr T midpoint(T a, T b) noexcept;

Для полноты вот немодифицированная реализация C++20 из статьи:

#include <type_traits>

template <typename Integer>
constexpr Integer midpoint(Integer a, Integer b) noexcept {
  using U = std::make_unsigned<Integer>::type;
  return a>b ? a-(U(a)-b)/2 : a+(U(b)-a)/2;
}

ожидает avg == 5.0 для этого теста

constexpr Integer midpoint(Integer a, Integer b) noexcept {
  using U = make_unsigned_t<Integer>;
  return a>b ? a-(U(a)-b)/2 : a+(U(b)-a)/2;
}
person Philipp Claßen    schedule 05.11.2019

    int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    decimal avg = 0;
    for (int i = 0; i < array.Length; i++){
        avg = (array[i] - avg) / (i+1) + avg;
    }

    int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    decimal avg = 0;
    for (int i = 0; i < array.Length; i++){
        avg = (array[i] - avg) / (i+1) + avg;
    }
тоже хороший способ.

person Toni Rossmann    schedule 22.12.2016

Предоставлено: http://www.ragestorm.net/blogs/?p=29

Вы не можете привести сумму к (((a&b << 1) + (a^b)) >> 1)?

person shubhros    schedule 12.03.2012
comment
Это просто неправильно, а не из-за переполнения. Он вычислит, что среднее значение 3 и 7 равно 8. Должно быть _1_. - person Alexey Frunze; 12.03.2012
comment
Как вы можете видеть, на Godbolt компилятор исследователь, это компилирует правильно, и так же версия, где мы изменяем операнды _3_, с тем же инлайн ассемблере. clang3.9, однако, делает беспорядок и решает использовать опцию _4_ для ограничения _5_, поэтому он сохраняет в памяти и использует операнд памяти. - person Joni; 03.10.2013