Вычисление времени компиляции количества битов, необходимых для кодирования n различных состояний

Редактировать: В исходном вопросе была неправильная формула, и испробованный алгоритм делал что-то совершенно отличное от того, что предполагалось. Прошу прощения и решил переписать вопрос, чтобы устранить всю путаницу.

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

constexpr unsigned bitsNeeded(unsigned n);

или через шаблон

Результаты должны быть:

+-----------+--------+
| number of | bits   |
| states    | needed |
+-----------+--------+
|     0     |    0   | * or not defined
|           |        |
|     1     |    0   |
|           |        |
|     2     |    1   |
|           |        |
|     3     |    2   |
|     4     |    2   |
|           |        |
|     5     |    3   |
|     6     |    3   |
|     7     |    3   |
|     8     |    3   |
|           |        |
|     9     |    4   |
|    10     |    4   |
|    ..     |   ..   |
|    16     |    4   |
|           |        |
|    17     |    5   |
|    18     |    5   |
|    ..     |   ..   |
+-----------+--------+

Исходный (как-то исправленный) вариант для справки:

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

ceil(log2(n))

constexpr unsigned ceilLog2(unsigned n);

Вот что я придумал (совершенно неправильно):

constexpr unsigned intLog2(unsigned num_states_) {
  return
    num_states_ == 1 ?
      1 :
      (
      intLog2(num_states_ - 1) * intLog2(num_states_ - 1) == num_states_ - 1 ?
        intLog2(num_states_ - 1) + 1 :
        intLog2(num_states_ - 1)
      );
}

Это дает правильный результат (для num_states_ != 0), но рекурсия вырывается экспоненциально, и она практически непригодна для любых входных данных, превышающих 10 (использование памяти во время компиляции превышает 2 ГБ , ОС зависает и компилятор падает).

Как я могу вычислить это во время компиляции на практике?


person bolov    schedule 21.05.2014    source источник
comment
Я не могу поверить, что это дает правильные результаты.   -  person Henrik    schedule 21.05.2014
comment
@ Хенрик, это не так. intLog2Runtime(256) = 16, intLog2Runtime(1024) = 32.   -  person DanielKO    schedule 21.05.2014
comment
@DanielKO GCC 4.8 жалуется на 1024...   -  person rubenvb    schedule 21.05.2014
comment
Кажется, вы пытаетесь вычислить квадратный корень, а не логарифм.   -  person Henrik    schedule 21.05.2014
comment
Минимальное количество битов будет двоичным логарифмом, округленным вверх (не вниз).   -  person Henrik    schedule 21.05.2014
comment
@ Хенрик, да, я перепутал и формулу, и алгоритм. Извини.   -  person bolov    schedule 21.05.2014


Ответы (7)


Минимальное количество битов, необходимое для хранения n различных состояний, равно ceil(log2(n)).

constexpr unsigned floorlog2(unsigned x)
{
    return x == 1 ? 0 : 1+floorlog2(x >> 1);
}

constexpr unsigned ceillog2(unsigned x)
{
    return x == 1 ? 0 : floorlog2(x - 1) + 1;
}

Обратите внимание, что ceillog2(1) == 0. Это прекрасно, потому что если вы хотите сериализовать объект и знаете, что один из его элементов данных может принимать только значение 42, вам не нужно ничего хранить для этого элемента. Просто назначьте 42 при десериализации.

person Henrik    schedule 21.05.2014
comment
@DanielKO похоже, что ваш компилятор сломан. ceillog2(2) == 1 ideone.com/6ztr7X - person Henrik; 21.05.2014
comment
моя ошибка, я неправильно вставил код; Я постоянно забываю, что вставка средней кнопкой ненадежна в текстовых веб-редакторах. - person DanielKO; 21.05.2014

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

constexpr unsigned numberOfBits(unsigned x)
{
    return x < 2 ? x : 1+numberOfBits(x >> 1);
}

Более простое выражение, правильный результат.

EDIT: "правильный результат", например, "предложенный алгоритм даже близко не подходит"; конечно, я вычисляю «количество битов для представления значения x»; вычтите 1 из аргумента, если вы хотите знать, сколько битов нужно считать от 0 до x-1. Для представления 1024 нужно 11 бит, для счета от 0 до 1023 (1024 состояния) нужно 10.

EDIT 2: функция переименована, чтобы избежать путаницы.

person DanielKO    schedule 21.05.2014
comment
Правильный результат? Уверены ли вы? - person Konrad Rudolph; 21.05.2014
comment
да, мой алгоритм делал что-то совершенно другое. Я сожалею об этом. Я ищу количество битов, необходимых для хранения n разных значений, например. для n = 4 требуется 2 бита. Ваше решение очень полезно, спасибо, легко адаптировать к тому, что я хочу. Еще раз извините за очень плохой вопрос. - person bolov; 21.05.2014
comment
еще раз извините за путаницу в вопросе. Ваша функция неверна, когда x является степенью числа 2. Например, для 8 состояний требуется только 3 бита. Ваш ответ дает 4. - person bolov; 21.05.2014
comment
@bolov либо вы не читали ×edit×, либо не пытались записать 8 в двоичном формате (1000, это 4 бита). - person DanielKO; 21.05.2014
comment
Предложите отличаться от ответа @bolov и удалить этот, он слишком сосредоточен на обсуждении... - person einpoklum; 17.07.2015

Из-за путаницы, вызванной первоначальным вопросом, я решил опубликовать этот ответ. Это основано на ответах @DanielKO и @Henrik.

Минимальное количество бит, необходимое для кодирования n различных состояний:

constexpr unsigned bitsNeeded(unsigned n) {
  return n <= 1 ? 0 : 1 + bitsNeeded((n + 1) / 2);
}
person Community    schedule 21.05.2014

может быть

constexpr int mylog(int n) {
    return (n<2) ?1:
           (n<4) ?2:
           (n<8) ?3:
           (n<16)?4:
           (n<32)?5:
           (n<64)?6:
           …
           ;
}

поскольку вы будете использовать его как параметр tempalte, вы можете проверить, что буст может предложить

person Valerij    schedule 21.05.2014
comment
да, я думал об этом, я просто надеялся, что до этого не дойдет. - person bolov; 21.05.2014

В C++20 у нас есть (в заголовке <bit>):

template<class T>
  constexpr T log2p1(T x) noexcept;

Возвраты: Если x == 0, 0; в противном случае один плюс логарифм x по основанию 2, при этом любая дробная часть отбрасывается. Примечания. Эта функция не должна участвовать в разрешении перегрузки, если T не является целым числом без знака.

person Marshall Clow    schedule 19.08.2019
comment
Кажется, это называется std::bit_width() официально - person robert4; 10.05.2021
comment
Сейчас сейчас; но это было не тогда, когда я написал этот ответ. - person Marshall Clow; 10.05.2021

constexpr немного слабоват и будет до C++14. Я рекомендую шаблоны:

template<unsigned n> struct IntLog2;
template<> struct IntLog2<1> { enum { value = 1 }; };

template<unsigned n> struct IntLog2 {
private:
  typedef IntLog2<n - 1> p;
public:
  enum { value = p::value * p::value == n - 1 ? p::value + 1 : p::value };
};
person Jon Purdy    schedule 21.05.2014
comment
Почему вы предлагаете шаблоны? То же самое тривиально возможно с constexpr. - person Konrad Rudolph; 21.05.2014
comment
@KonradRudolph: Я подозреваю, что, поскольку шаблоны использовались таким образом дольше, чем constexpr, компилятор может быстрее оценить этот перевод 1: 1. Однако я должен был подумать об изменении алгоритма, и я могу понять, если ваше возражение носит эстетический характер. - person Jon Purdy; 21.05.2014

Что-то, что я использовал в своем собственном коде:

static inline constexpr
uint_fast8_t log2ceil (uint32_t value)
/* Computes the ceiling of log_2(value) */
{
    if (value >= 2)
    {
        uint32_t mask = 0x80000000;
        uint_fast8_t result = 32;
        value = value - 1;

        while (mask != 0) {
            if (value & mask)
                return result;
            mask >>= 1;
            --result;
        }
    }
    return 0;
}

Он требует, чтобы C++14 использовался как constexpr, но у него есть приятное свойство, заключающееся в том, что он достаточно быстр во время выполнения — примерно на порядок быстрее, чем при использовании std::log и std::ceil — и я убедился, что он дает те же результаты для все представимые ненулевые значения (логарифм не определен для нуля, хотя 0 является разумным результатом для этого приложения; вам не нужны биты, чтобы различать нулевые значения), используя следующую программу:

#include <iostream>
#include <cstdlib>
#include <cstdint>
#include <cmath>
#include "log2ceil.hh"

using namespace std;

int main ()
{
    for (uint32_t i = 1; i; ++i)
    {
        // If auto is used, stupid things happen if std::uint_fast8_t
        // is a typedef for unsigned char
        int l2c_math = ceil (log (i) / log (2));
        int l2c_mine = log2ceil (i);
        if (l2c_mine != l2c_math)
        {
            cerr << "Incorrect result for " << i << ": cmath gives "
                 << l2c_math << "; mine gives " << l2c_mine << endl;
            return EXIT_FAILURE;
        }
    }

    cout << "All results are as correct as those given by ceil/log." << endl;
    return EXIT_SUCCESS;
}

Это также не должно быть слишком сложно обобщить на различную ширину аргумента.

person Stuart Olsen    schedule 21.05.2014