Алгоритмы поиска старшего бита

    Здесь я хочу рассказать и обсудить несколько алгоритмов для нахождения старшего единичного бита числа.

    На всякий случай, поясню: старшим битом называется единичный бит числа, отвечающий за самую большую степень двойки. Иными словами, это самая большая степень двойки, не превосходящая числа. Чтобы избежать многих случаев, будем здесь считать, что мы имеем дело с натуральным числом в пределах от 1 до 2^31 — 1 включительно. Кроме того, чтобы не слишком углубляться в теорию вероятности, будем считать, что число, в котором требуется определить старший бит, с одинаковой вероятностью будет любым из возможных чисел.

    Для начала, рассмотрим самый простой, первым приходящий в голову алгоритм. Давайте переберём все степени двойки, и выберем из них максимальную, которая не превосходит числа. Здесь, очевидно, можно воспользоваться монотонностью этого свойства, то есть тем, что если какая-то степень двойки не превосходит числа, то и меньше степени и подавно не превосходят. Поэтому, это метод можно написать очень просто:

    int bit1(int x) {
       int t = 1 << 30;
       while (x < t) t >>= 1;
       return t;
    }
    


    (тут я использую java, но, думаю, понятно будет всем, в силу нативности кода)

    Посмотрим, как долго он может работать. Если считать, что число с одинаковой вероятностью оказывается любым из обозначенного промежутка, то, с вероятностью 1/2, а именно, если х окажется не меньше, чем 2^30, цикл while не отработает ни одного раза. С вероятностью 1/4, а именно, если х находится в промежутке от 2^29 до 2^30 — 1, цикл отработает один раз. И так далее. Несложно понять, что это означает, что цикл отрабатывает в среднем полраза. Что весьма неплохо в среднем, но плохо в худшем случае: на числе х=1 цикл отработает все тридцать раз.

    Следующий алгоритм лишён этой неприятности; а точнее, он лишён неопределённости во времени работы вообще. Я для начала продемонстрирую код, а потом объясню принцип работы.

    int bit2(int x) {
       x |= x >> 1;
       x |= x >> 2;
       x |= x >> 4;
       x |= x >> 8;
       x |= x >> 16;
       return x - (x >> 1);
    }
    


    Итак, пусть дано число х=000001bbbbb (я не следил за необходимым количеством бит в числе, b означает любой бит). Тогда
    x          == 000001bbbbb
    x >> 1     == 0000001bbbb
    x | x >> 1 == 0000011bbbb
    

    Таким образом, первое действие вслед за старшей единичкой, где бы она не оказалась, ставит следующую. Второе, как можно понять, ставит за этими двумя ещё две. Третее — ещё 4 (если есть, куда ставить). И так далее. Таким образом, в числе все биты после старшего оказываются единичными. Тогда понятно, что x — (x >> 1) выдаёт нам правильный ответ.

    Третий алгоритм не совсем лишён произвола во времени работы, но в худшем случае, тем не менее, работает значительно быстрее первого. Он основан на бинпоиске. Попробуем взять средний бит, например, 16-тый, и сформируем условие на то, будет старший бит младшей 16-го, или нет. Понятно, что таким условием будет x < 1 << 16. Значит, надо сохранить результат проверки:

    int t = 1;
    if (x >= 1 << 16) t <<= 16;
    


    ну а дальше, конечно, надо проверить, нельзя ли подвинуть t ещё на 8 бит, потом на 4, на 2, и на 1:

    int bit3(int x) {
       int t = 1;
       if (x >= t << 16) t <<= 16;
       if (x >= t << 8) t <<= 8;
       if (x >= t << 4) t <<= 4;
       if (x >= t << 2) t <<= 2;
       if (x >= t << 1) t <<= 1;
       return t;
    }
    


    Итак, второй и третий алгоритмы работают в среднем дольше, чем первый (что и подтверждает непосредственный эксперимент, а так же то, что третий алгоритм работает чуть-чуть быстрее второго), но в худшем случае они работают быстрее.
    Кроме того, надо заметить одну вещь. В первом методе используется магическая константа 1 << 30. Её использование основано на том, что мы знаем, что число с равной вероятностью бывает любым от 1 до 2^31 — 1. Если числа бывают меньшими, то можно уменьшить и константу. Например, если числа от 1 до 100000, то можно начинать с int t = 1 << 17. Но что если мы не знаем, какими будут числа, для которых используется метод? Если они теоретически могут быть равными до 2^31 — 1, а на самом деле они будут не больше 1000. Тогда нам придётся поставить int t = 1 << 30, и этот метод (как, опять же, подтверждают эксперименты), будет работать значительно дольше, чем последующие два. То есть, первый метод не только может иногда работать долго, но и может оказаться, что он в среднем работает дольше. Время его работы может оказаться непредсказуемым. Тогда как все эти непряитности совершенно не относятся к остальным двум методам.

    Наконец, ещё один нюанс. Три вышеперечисленных алгоритма возвращают именно степень двойки — число вида 2^k. Но что, если нам надо найти именно k? Первый и третий методы легко преобразуются так, чтобы выдавать показатель степени, а не саму степень. Первый алгоритм начинает работать несколько быстрее, третий — чуть-чуть дольше. Но второй алгоритм к этому совершенно не приспособлен. Таким образом, второй алгоритм тоже имеет свой специфический минус.

    P.S. Первый алгоритм слишком очевиден, чтобы ставить вопрос об авторстве, второй подсказан мне одним односительно широко известным олимпиадным программистом, а третий я придумал сам, хотя убеждён, что далеко не единствнным и уж точно не первым.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 100
    • 0
      Сразу подумал о том что будет бинпоиск. Угадал =). Но в общем то странно, зачем нужно решать именно такую задачку. Время работы в обоих случаях очень малы и чтобы почувствовать разницу, нужны очень большие ограничения. Да и вообще, за всё время ни разу не приходилось использовать побитовые операции, и хитрым образом хранить много данных в одной переменной.
      • 0
        Ответ ниже, промахнулся ссылкой.
      • +1
        Очень большие ограничения — это вряд ли, Вы правы, зато может понадобиться делать это много раз. Я делал миллирд раз (1000 раз на миллионе рандомных чисел), и времена получались для первого метода порядка 0.8 секунды, для следующих двух — порядка 3 секунд.
        Я согласен, в работе это может пригодиться, если только очень повезёт. Не могу сказать, что я в работе не использовал битовые операции и не хранил сразу много данных в одном числе, но у меня работа в данный момент спецэфическая, надо чтобы сложный алгоритм распознавания образов работал очень-очень быстро, вот и оптимизирую, как только могу.
        Это, скорее, может быть интересно олимпиадным программистам. Мне на олимпиадах, по-моему, один раз приходилось это делать.
        Но суть-то не в этом — просто задачка интересная, и методы решения тоже)
        • –1
          Я занимаюсь олимпиадным программированием, но что то не впечатлила меня задачка. Может быть я просто ещё не дорос до того уровня чтобы оптимизировать каждый малейший кусочек кода.
          Рассказали бы про распознавание образов, гораздо более интересная тема.
          • +1
            Может, когда-нибудь и расскажу.
            Да этим в основном марафонцы занимаются, оптимизированием каждого кусочка кода.
            • 0
              Ну, например, для реализации binary indexed tree нужно решить противоположную задачу — найти последний единичный бит числа. Редко звучит на соревнованиях, но бывает.
              Вполне возможно, что где-то и старший может пригодиться. :-)
              • 0
                Младший единичный — в одну команду: return a&-a;
              • 0
                А что, никто из порицающих статью, не сталкивался с «длинной арифметикой»?
                Алгоритмы RSA, DH, ElGamal, когда числа длиной по 2048 бит.
                Нужен поиск старшего значащего бита, объективно нужен.
                И если удастся его оптимизировать — это тоже хорошо и значимо.
                • 0
                  Согласен, однако методы, предложенные в статье, при использовании длинной арифметики не подходят
            • +6
              Второй алгоритм хорош тем, что в нём нет условных переходов. Соответственно, блок предсказания переходов не сможет ошибиться никогда.

              Ссылки по теме:

              graphics.stanford.edu/~seander/bithacks.html
              www.inwap.com/pdp10/hbaker/hakmem/hakmem.html
              aggregate.org/MAGIC/
              • 0
                Это в принипе, конечно, верно, но, как показывает практика (то бишь, эксперименты), третий алгоритм в среднем даже немного быстрее. Кроме того, не думаю, что на пяти условных переходах можно опрелённо сказать, была ли работа предсказателя положительной или отрицательной — это слишком мало, здесь он не совершает никакой осмысленной работы.

                За ссылки спасибо)
                • 0
                  Только что проверил. CPU: i5-750. 100 запусков по 10 миллионам вызовов, среднее время:

                  bit2: 193 мс
                  bit3: 227 мс
                  • –2
                    Хм. Ну, возможно, мои измерения были не совсем точны, и второй алгоритм действительно немного быстрее. Может быть, дело в том, что у Вас многоядерный процессор, а у меня одноядерный? Я, если честно, не очень хорошо в этом разбираюсь, но, может быть, это могло как-то повлиять.
                    • +1
                      Думаю, язык программирования и компилятор всё же важнее количества ядер при выяснении оптимального алгоритма.
                      • 0
                        g++ (Debian 4.4.2-9) 4.4.3 20100108 (prerelease)
                        • +1
                          java 1.6.0_18
                          Да, может быть, особенности языка.
                          • 0
                            в java кстати используется именно второй алгоритм:

                            java.lang.Long
                            @since 1.5
                            public static long highestOneBit(long i) {
                            // HD, Figure 3-1
                            i |= (i >> 1);
                            i |= (i >> 2);
                            i |= (i >> 4);
                            i |= (i >> 8);
                            i |= (i >> 16);
                            i |= (i >> 32);
                            return i — (i >>> 1);
                            }
                      • 0
                        даже в одноядерном проце действия распараллеливаются http://en.wikipedia.org/wiki/Pipeline_(computing)
                        • 0
                          Если измеряете на Java, учитывайте, какой компилятор работает: C1 или C2. Разница существенна, как видно из комментариев выше.
                            java -client всегда будет использовать C1
                            java -server всегда будет использовать С2
                          
                        • 0
                          Почему выбирается среднее время, а не минимальное?
                          • 0
                            Мне лично минимальное время считать сложнее, поскольку java не очень точна, когда меряешь маленькое время, значит, нужно делать много раз каждый тест, и, желательно, много тестов, по секунде на тест — это долго.
                            • 0
                              Вы бредете?
                              фраза — «поскольку java не очень точна», убила наповал.
                              • 0
                                Читайте полностью: «java не очень точна, когда меряешь маленькое время». Имеется в виду, что System.nanoTime(), который я использовал, выдаёт неточный результат. И чем меньше время, которое пытаешься с его помощью померять, тем больше относительная погрешность.
                                • 0
                                  Неточный для чего? для запуска баллистических ракет?

                                  Можно увеличить кол-во вызовов, например, и будет вам ещё большее время. Причём тут наносекунды вообще? %)
                                  • 0
                                    Что значит, для чего? Он просто не точный. Прогрешность хуже, чем одна наносекунда.

                                    вы вообще читать умеете? Цитата из моего коммента:
                                    «значит, нужно делать много раз каждый тест, и, желательно, много тестов»

                                    Чтобы была нормальная точность, надо делать каждый тест много раз — по моему опыту, миллион даже будет маловато, 10 млн минимум. А то я уже получал какие-то фантастические результаты замеров времени, когда делал 1 000 000 тестов, и время измерялось в милисекундах. А потом всё становилось на свои места, когда я увеличивал количество тестов до миллиарда.
                                    А для репрезентативности выборки надо делать много разных тестов. Вот и получается, что надо делать много раз по секунде.
                            • 0
                              На самом деле кроме среднего ещё считалась дисперсия. Дисперсия с первого раза вышла небольшая (и оставалась небольшой при повторных запусках), поэтому я считаю, что среднему доверять можно.
                              • 0
                                Выполнение кода, это не физический процесс. Другими словами, истинное время выполнения кода, без побочных эффектов, будет ближе именно к минимальному времени выполнения, т.к. все программы и процессы, выполняемые на компьютере, только добавляют погрешность, а нам важно чистое время выполнения кода без влияние внешних факторов.
                                • 0
                                  Время выполнения третьего алгоритма зависит от данных.
                      • –1
                        по-моему, задача слишком мелкая, чтоб уделять ей столько внимания
                        • +3
                          Да блин, неужели задача должна быть полезной, чтобы быть интересной?
                          • –8
                            Вы можете не согласиться… но как бы да
                            • +12
                              Вы заставляете математиков плакать.
                            • 0
                              вобщем-то нет
                          • +4
                            Только что наткнулся на еще одну реализацию, но на ассемблере :)

                            mov bx, 41h
                            bsr cx, bx ;cx=06h
                            • НЛО прилетело и опубликовало эту надпись здесь
                              • 0
                                Вроде как, надо еще проверить ZF, если в вашем примере bx == 0, то содержимое cx не определено.
                              • +1
                                Сбросить флаг переноса
                                Сдвигать через перенос по счетчику, проверяя на JNC условие.

                                В три четыре команды процессора бы уложился. Не оно?
                                • +1
                                  О, я так не понимаю. Буду рад, если Вы поясните) Особенно фраза «три-четыре команды» звучит заманчиво — ведь это противоречиит моим представлениям о некоторых теоретических результатах в этой области. Буду рад узнать, что я ошибался.
                                  Но вообще, конечно, обсуждаются реалзации на языке высокого уровня.
                                  • +4
                                    Перенос — это флаг С есть по моему во всех процессорах без исключения. Особый флаг состояния процессора, сигнализирующий обычно о заеме или переполнении байта при выполнении предыдущей операции.

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

                                    А все ветвление в процессоре построено на флагах. Т.е. наличие или отсутствие флага это повод выполнить или игнорировать команду перехода. Флаг переноса это один из таких флагов (есть еще флаг нуля, переполнения, полупереносов, и еще с пол десятка разных)

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

                                    Как только флаг показался. выходим из цикла и число в нашем счетном регистре даст номер первого старшего бита.

                                    Ну и некоторые архитектуры вроде х86 имеют циклические префиксы команд, позволяющие любую команду выполнять много раз, в качестве счетчика цикла использующие один из регистров.

                                    Вообще советую побаловаться ассемблером, прикольный язык. На х86 смотреть смысла особого уже нет, а вот всякие микроконтроллеры вроде AVR или PIC это то что доктор прописал. Заодно научитесь немного рулить железом. Если заинтересовало, то велкам ко мне на сайт :)
                                    • +2
                                      У вас получился первый алгоритм с циклом, только самым лучшим образом портированный (если можно так сказать) на ассемблер. С такими же сравнительными характеристиками времени работы.
                                      • 0
                                        Круто, спасибо за такой подробный комментарий, было интересно.
                                        Правда, как уже заметили ruguevara и mephisto, сточки зрения алгоритма — этот, по сути, такой же, как первый.
                                    • 0
                                      с точки зрения алгоритма — первый вариант, немного измененный.
                                      • 0
                                        Во, мне так и показалось. Возможно, слово «сдвигать» на это указывает.
                                        • 0
                                          в данном случае флаг переноса — как бы 33ий бит, добавляемый к регистру, и устанавливается при сдвиге влево операнда, у которого установлен старший бит.
                                          • 0
                                            о, спасибо. А сдвигать через перенос по счётчику — это как расшифровывается?
                                            • 0
                                              Это что то вроде…

                                              while (t<(1<<32)) t<<=1;

                                              для 64битного t и при условии что t не может быть >= (1<<32)
                                              • 0
                                                Я во всех примерах счетчики опускаю для простоты)
                                            • 0
                                              Ну а JNC — условный переход при установленном флаге переноса. Используются особенности архитектуры процессора для оптимизации.
                                              • 0
                                                Впрочем, если подумать, по сути это абсолютно аналогично варианту

                                                while (t) t>>=1;

                                                только в обратную сторону.
                                                вместо флага переноса тут используется флаг zero
                                            • +3
                                              bsfl на x86 (находит первый установленный бит в машинном слове)
                                              • +1
                                                Какая прелесть. В одну команду прям?
                                                • 0
                                                  Ага, но этот, хм.., алгоритм ограничен аппаратным машинным словом и на многоразрядную арифметику не масштабируется. Но это можно использовать эту команду скрестив с бинпоиском:
                                                  1. делим длинное число на машинные слова (int)
                                                  2. берем серединное слово из числа, вычисляем одной машинной командой его старший бит.
                                                  3. Если он есть, запоминаем, берем слово из середины второй половины числа, если его нет, берем слово из середины первой половины числа.
                                                  • 0
                                                    Неееет, не годится. Даже если серединное слово полностью нулевое — в более старших словах могут, тем не менее, быть ненулевые биты.
                                                    • 0
                                                      Правильно, поэтому надо продолжать бинпоиск в правой части, как я и написал
                                                      • 0
                                                        Вы написали — из первой части, то есть, из младшей. У нас с Вами конфликт обозначений) Поэтому предлагаю употреблять термины «старший» и «младший», как не подлежащие сомнению.

                                                        Но если серединное слово ненулевое — то тоже надо продолжать поиск в старшей части. Перейти к младшей можно, только проверив всю старшую половину.
                                                        • 0
                                                          да, я не прав, такой алгоритм не сработает. Метод «разделяй на слова и оптимизируй поиск старших битов» надо применять по-другому.
                                                      • 0
                                                        … в смысле в старшей
                                              • НЛО прилетело и опубликовало эту надпись здесь
                                                • 0
                                                  пффф)) Ну да, Вы, безусловно правы) Этот алгоритм тоже имеет право на существование, и у него есть свои спецИфические плюсы и минусы =)
                                                  • +1
                                                    а не великовата ли таблица будет))

                                                    как вариант, можно немного усложнить, и сделать таблицу для байта, а потом искать старший байт с единичным битом, и высчитывать результат.
                                                    • 0
                                                      Ну да) Для слишком больших типов (уже для 32-х битных) табличка будет уже слишком большой — это и есть специфический минус =)
                                                      Но и Вы правы, да. В зависимости от ситуации, возможно, можно сделать и для двух байтов табличку.
                                                  • –2
                                                    Что-то я туплю, видимо.
                                                    Чем x & (2^32) не подходит?
                                                    • +1
                                                      Ну, пусть мы будем рассматривать long (или long long в С++, короче, 64-битный тип. Иначе 2^32 == 0).
                                                      Тогда ваше выражение выдаёт два возможных ответа: 0 или 2^32. Очевидно, что возможных ответов в этой задаче гораздо больше.
                                                      & — это побитовое логическое И, не так ли?
                                                      • –2
                                                        & — битовая операция. То есть размер типа мы не знаем, так что ли?
                                                        • 0
                                                          Знаем, конечно.
                                                          Так, ещё раз. В статье шла речь о числах от 1 до 2^31-1. То есть, о тех, у кого единичными могут быть только биты от 0-го до 30-го. В упомянутом Вами числе единственный ненулевой бит — 32-ой. Значит, не будет ни одного бита, который был бы единичным и в х, и в 2^32. Значит, все биты выражения x & (2^32) будут нулевыми. То есть, результат этого выражения будет равен нулю.
                                                          • 0
                                                            Если размер числа мы знаем, то значение старшего бита находится вот так: x & (2^sizeof(type)), нет?
                                                            • 0
                                                              Цитата из поста:
                                                              «На всякий случай, поясню: старшим битов называется единичный бит числа, отвечающий за самую большую степень двойки. Иными словами, это самая большая степень двойки, не превосходящая числа.»
                                                              Здесь, разумеется, имеется в виду само по себе натуральное число, а не то, сколько места оно занимает в памяти.
                                                              • 0
                                                                Ффух, вот теперь понял, спасибо за терпение :)
                                                                • 0
                                                                  В следующий раз читайте внимательней.
                                                    • +2
                                                      Может быть кому-нибудь понадобится. Куча битхаков: graphics.stanford.edu/~seander/bithacks.html
                                                      • 0
                                                        Последние два алгоритма имхо — методы половинного деления.
                                                        • 0
                                                          Ну, в третьем так и написано, а про второй — я не согласен. Там ничего не делится пополам.
                                                          • 0
                                                            Сорри туплю. Пойду просплюсь ;)
                                                        • 0
                                                          Потестите, кто конечный автомат на базе лукап-таблицы. Сам использую несколько модифицированный вариант — в моем применении оказался наиболее быстрым.
                                                          • +4
                                                            Java использует в Integer.highestOneBit() в точности второй способ.
                                                            Так что скорее всего это один из наиболее оптимальных.
                                                            Интересно, что младший бит можно найти за одну инструкцию:
                                                            x & -x;
                                                            • 0
                                                              Да) Я, когда об этом задумался, сначала понял, что все три алгоритма с успехом модифицируются для поиска младшего бита, а потом сообразил, что с младшим всё гораздо проще)
                                                              • 0
                                                                Почему ява не использует native-код в виде пары команд ассемблера?
                                                                • 0
                                                                  Вопрос скорее к создателям java, но, думаю, это и так очевидно. Чего ради им жертвовать высокоуровневостью и безопасностью?
                                                                  • 0
                                                                    с каких пор арифметические команды x86 стали небезопасными?
                                                                    • 0
                                                                      с того же самого, с чего и половина jvm описана как native
                                                                      • 0
                                                                        Так или иначе, вопрос не ко мне)
                                                                    • 0
                                                                      В Джаве, как языке, не может быть ассемблерных инструкций. По возможности код, в том числе системных классов, пишется кросплатформенным образом. Однако для некоторых методов, особенно критичных к производительности, Just-in-Time компилятор может обходить (игнорировать) написанный Java-код, подменяя его асcемблерными инструкциями (так называемые Intrinsic methods). Например, так делается для всяких там String.indexOf, Integer.bitCount, System.arraycopy и т.п.
                                                                      Очевидно, Long.highestOneBit не является критичным для типичных Java-приложений, и для него нет VM Intrinsics.
                                                                  • –4
                                                                    глупо тратить время на подобные алгоритмы, когда логарифм с основанием 2 выполняется аппаратно
                                                                    • +2
                                                                      я уже проверил, работает медленнее.
                                                                      алгоритм | на случайных данных | на 1  | на 0x7fffffff | на 0x3fffffff 
                                                                      bit1     | 25.06               | 255.93| 17.59         | 23.10
                                                                      bit2     | 40.51               |  39.27| 39.90         | 38.91
                                                                      bit3     | 40.15               |  26.64| 40.94         | 39.41
                                                                      log2     | 62.88               |  62.65| 62.75         | 62.63
                                                                      
                                                                      

                                                                      поправка на цикл и вызов функции: 12.71
                                                                      • +2
                                                                        конечно я использовал frexp он значительно быстрее log2
                                                                    • +2
                                                                      В книги Генри Уоррена «Hacker's Delight» таких алгоритмов масса. Рекомендую к прочтению.
                                                                      • 0
                                                                        А оптимальным таки будет таблица на 32/64/128 элементов и поиск диапазон сдвигом на 5/6/7 бит — т.е. размен память-быстродействие. Особенно полезно для всякого рода микроконтроллеров, где быстродействие ограничено, а память программ, как правило есть с некоторым запасом.
                                                                        • +1
                                                                          Эх спектрум спектрум, как много в этом слове…
                                                                          START LD A,[input]
                                                                          PUSH BC
                                                                          LD B,128
                                                                          LOOP RL A
                                                                          JR C,EXIT
                                                                          RR B
                                                                          JR NC,LOOP
                                                                          EXIT LD A,B
                                                                          POP BC
                                                                          RET
                                                                          • 0
                                                                            А еще есть ffs в С. Алгоритмы знать полезно, но лучше использовать стандартную функцию, если это возможно.
                                                                            • 0
                                                                              Замечание в целом верное (когда речь касается промышленного программирования, а не олимпиадного), но сразу замечу, что вопрос исследовался скорее из интереса, нежели для пользы дела.

                                                                              Если же говорить о деле, то изредка всё же лучше писать своё, даже если возможно использовать стандартный метод — и для этой цели в статье в некоторой степени исследован вопрос о том, в каких случаях какой метод лучше применить.
                                                                              • 0
                                                                                ffs ищет первый младший бит. Так что это другое.
                                                                              • 0
                                                                                два вопроса:
                                                                                1. зачем нужно вычислять номер самого старшего бита?
                                                                                2. в процессоре есть команда для этого вычисления. вторая ссылка яндекса: computers.plib.ru/programming/Assembler/Pr/Index4.htm. зачем писать код на ЯВУ для этого?
                                                                                • 0
                                                                                  Слушайте, в комментариях уже есть ответы на оба вопроса. Вы бы читали, прежде, чем спрашивать.
                                                                                • +1
                                                                                  Табличный алгоритм уже упоминали, а я, ради интереса, реализовал и проверил следующий вариант (оптимальный, на мой взгляд, по соотношению скорость-память). Таблицы большего размера хуже еще тем, что они не всегда будут полностью умещаться в кэше процессора.

                                                                                    int bit4(int x) {
                                                                                       if ((x >>> 24) != 0) { return table24[x >>> 24]; }
                                                                                       if ((x >>> 16) != 0) { return table16[x >>> 16]; }
                                                                                       if ((x >>> 8)  != 0) { return table8 [x >>> 8];  }
                                                                                       return table0[x];
                                                                                    }
                                                                                  


                                                                                  В Джаве этот работает чуть быстрее, чем первые три, на C же данный метод оказывается лучше в 3-5 раз!
                                                                                  • 0
                                                                                    Надо же. Язык реализации, стало быть, и впрямь очень важен.
                                                                                    А стоит ли создавать 4 таблицы? По скорости выигрываем всего лишь одно сложение, это не скажется сколь-нибудь существенно на скорости работы, а по памяти проигрываем в 4 раза!
                                                                                    • 0
                                                                                      Пожалуй, да — такая «оптимизация» уже излишняя, одной таблицы достаточно. На C разница в производительности 4%.
                                                                                  • 0
                                                                                    Исходное число держим в EAX…
                                                                                    xor ecx, ecx
                                                                                    @@:
                                                                                    shr eax,1
                                                                                    loopnz @@
                                                                                    neg ecx
                                                                                    

                                                                                    труЪ чистый ассемблер рулит!

                                                                                    P.S. Может, на MMX, SSE1/2/3… задача вообще 1 командой решается?
                                                                                    • 0
                                                                                      Не, в исходной статье не номер бита ищут, а сам бит.
                                                                                      А так, во многих процессорах есть команда clz (или nlz?) — число ведущих нулей.
                                                                                      • +1
                                                                                        тогда добавляем
                                                                                        stc
                                                                                        rcl eax, cl
                                                                                    • +1
                                                                                      Самый быстрый алгоритм, работает даже на i386 :)
                                                                                      ; аргумент - в EAX
                                                                                      bsr ecx, eax
                                                                                      ; если хотим получить не номер бита (в жизни бывает надо чаще), а соответствующее ему число
                                                                                      mov eax, 1
                                                                                      shl eax, cl
                                                                                      ; результат получаем туда же - в EAX
                                                                                      

                                                                                      Итак: всего 3(!) машинных команды, i386+

                                                                                      Мораль подтверждается в очередной раз: при прочих равных одна и та же задача решается программным кодом разной тяжести, а скрытые издержки рождаются и устраняются на низком уровне
                                                                                      Если переписывать на ассемблере софт наших информационных систем конечно же нерационально, то надо хотя бы быть разборчивыми к тому, какой груз лишнего кода мы скармливаем процессорам рабочих станций и продакшн-серверов — это есть необходимое условие для того чтобы IT было Lean = без скрытых потерь.

                                                                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.