Вычисление sqrt(SIZE_MAX+1) с использованием только целочисленных константных выражений, учитывающих странные ABI

Библиотека C OpenBSD имеет расширение под названием reallocarray(3), которое делает realloc(array, size*nmemb) без взрыва, если умножение переполняется. Реализация содержит этот фрагмент:

/*
 * This is sqrt(SIZE_MAX+1), as s1*s2 <= SIZE_MAX
 * if both s1 < MUL_NO_OVERFLOW and s2 < MUL_NO_OVERFLOW
 */
#define MUL_NO_OVERFLOW (1UL << (sizeof(size_t) * 4))

На сайте Programmers.SE модифицированная версия этого расчета была отклонена за техническую некорректность. 4, очевидно, должно быть CHAR_BIT/2, но это не единственная проблема. Предположим необычный ABI, в котором size_t имеет биты заполнения. (Это не до смешного неправдоподобно: рассмотрим микроконтроллер с 32-битными регистрами, но 24-битным адресным пространством.) Тогда SIZE_MAX меньше, чем 1 << (sizeof(size_t)*CHAR_BIT) [в арифметике с бесконечной точностью], и вычисление неверно.

Итак, вопрос: можете ли вы вычислить floor(sqrt(SIZE_MAX+1)), используя только арифметику целочисленного константного выражения C99, не делая никаких предположений о ABI, кроме того, что требует C99, плюс то, что вы можете узнать из <limits.h>? Обратите внимание, что SIZE_MAX может равняться UINTMAX_MAX, т. е. не может быть никакого типа, который может представлять SIZE_MAX+1 без переполнения.

EDIT: я думаю, что SIZE_MAX должно быть 2n 1 для некоторого положительного целого числа n, но не обязательно в форме 22n 1 рассмотрим S/390, один из ABI которого имеет 31-битное адресное пространство. Поэтому: Если sqrt(SIZE_MAX+1) не является целым числом, желаемый результат (учитывая, как используется эта константа) составляет floor() от истинного значения.


person zwol    schedule 02.09.2014    source источник
comment
Вы можете использовать жестко заданное количество итераций вавилонского (Герона) метода en.wikipedia.org/wiki /, однако вам придется делать предположения о максимально возможном размере слова.   -  person Peter G.    schedule 02.09.2014
comment
А как насчет некоторых действительно правдоподобных предположений, например, MUL_NO_OVERFLOW является степенью двойки?   -  person zch    schedule 02.09.2014
comment
Можно ли с уверенностью предположить, что SIZE_MAX+1 является степенью двойки, а 2¹⁶ ≤ SIZE_MAX ≤ 2⁶⁴? Если это так, это будет означать, что существует не более 49 возможных значений, и можно использовать оператор ?:, чтобы выбрать одно из них. Используя некоторые вложенные макросы, соответствующее выражение может быть достаточно кратко выражено в исходном коде.   -  person supercat    schedule 02.09.2014
comment
@zch Я думаю, что целочисленные типы без знака должны иметь _MAX форму 2^N-1 для некоторого N, но у меня нет моей копии стандарта на этом компьютере.   -  person zwol    schedule 02.09.2014
comment
Это правда, но я не думаю, что это должно быть 2^2n .   -  person zch    schedule 02.09.2014
comment
@zch Мм, да. Опять же, я не помню точно, что говорит стандарт, но я знаю, что были системы, в которых абстрактно правильным выбором для SIZE_MAX было 2^31-1.   -  person zwol    schedule 02.09.2014
comment
Грубая идея: #define M SIZE_MAX #define S2 1.41421... #define MUL_NO_OVERFLOW (size_t) ((((M&1)?S2:1) * ((M&2)?2.0:1) * ((M&4)?2.0*S2:1) * ((M&16)?4.0:1) ... ) + 0.0001).   -  person chux - Reinstate Monica    schedule 02.09.2014
comment
@chux Арифметика с плавающей запятой не может использоваться в целочисленной константе.   -  person zwol    schedule 02.09.2014
comment
Почему нельзя использовать арифметику с плавающей запятой, например #define X ((int) (1.5*1.5)), для формирования int y[X];?   -  person chux - Reinstate Monica    schedule 02.09.2014
comment
@chux Потому что стандарт говорит, что вы не можете. (Или, если хотите, потому что стандарт не требует, чтобы реализация могла вычислять выражения с плавающей запятой во время компиляции.)   -  person zwol    schedule 02.09.2014
comment
Было бы мошенничеством предположить SIZE_MAX == power(2,n)-1, а затем #if SIZE_MAX == 65535 #define MUL_NO_OVERFLOW 256 #else if SIZE_MAX == 131072 #define MUL_NO_OVERFLOW 362 #else if SIZE_MAX == 262144 #define MUL_NO_OVERFLOW 512 ... #endif? (Имейте случай для каждого (16 ‹= n ‹= 64 или 128) Нет баллов за элегантность, но, похоже, работа выполнена.   -  person chux - Reinstate Monica    schedule 02.09.2014
comment
Я не уверен, нужен ли вам этот тест вообще, но вы можете получить еще лучшие результаты с (nmemb >= SIZE_MAX/256 || size >= 256), минуя всю проблему квадратного корня.   -  person zch    schedule 03.09.2014


Ответы (3)


Константа SIZE_MAX неотрицательна и имеет тип size_t. Для краткости определю:

  #define S SIZE_MAX  

Математическое значение S+1 находится или может быть, как вы указали, за пределами диапазона для любого целочисленного типа.
Я напишу S1 для математического значения S+1.
Если мы рассмотрим логарифм (по основанию 2, если хотите) из S1, то имеем:

 logarithm(sqrt(S1)) == (1.0/2.0) logarithm(S1)

С другой стороны, почти наверняка в каждой реальной ситуации мы будем иметь, что S представлено как двоичное число, имеющее только 1 бит. Число b этих битов есть, вообще, число CHAR_BIT, умноженное на степень двойки, умноженное на CHAR_BIT: 16, 32, 64, 128... Показатель степени этой степени я буду обозначать p. Таким образом, для CHAR_BIT == 8 имеем:

16 == CHAR_BIT * 2 ----> p == 1
32 == CHAR_BIT * 4 ----> p == 2
64 == CHAR_BIT * 8 ----> p == 3

Теперь у нас есть:

logarithm(S1) == b == CHAR_BIT * (2 ** p)   (I am denoting with ** to the "power math. operator").

logarithm(sqrt(S1)) == logaritm(S1) / 2.0 == CHAR_BIT * (2 ** p) / 2.0 == CHAR_BIT * (2 ** (p - 1))

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

sizeof(size_t) == b == CHAR_BIT * (2 ** p)

Можно предположить, что касается состояния дел в 2014 году, что значение p <= 5, скажем (в дальнейшем вы можете увеличить это магическое число 5 до больших значений).

Теперь рассмотрим следующее выражение, предназначенное для поиска и нахождения значения b в предположении, что p <= 5:

#define S_1 ((size_t)1ULL)
#define b (sizeof(size_t))
#define bitexpr(p) ((size_t)(CHAR_BIT * (S_1 << (p))))
#define expr(p) ((size_t) (S_1 << (p)))
#define exp2_expr_1(p) ((size_t)(S_1 << bitexpr(p-1)))
// SRSM() stands for: Square Root SizeMax
#define SRSM  ( \
    (expr(1)==b)? exp2_expr_1(1) :             \
        (expr(2)==b)? exp2_expr_1(2) :         \
            (expr(3)==b)? exp2_expr_1(3) :     \
              (expr(4)==b)? exp2_expr_1(4) :   \
                (expr(5)==b)? exp2_expr_1(5) : \
                   (size_t)0  /* Error! */     \
    ) /* end-of-macro*/

Макрос SRSM выводит, на самом деле, квадратный корень из S+1, но я полагаю, вы можете придумать, что делать с этим числом.

Здесь важно то, что квадратный корень из SIZE_MAX можно получить с помощью чисто целочисленных константных выражений.

При желании «волшебную» цифру 5 можно заменить на другую.

Более общий подход, предназначенный для решения произвольной ситуации на любой возможной машине, соответствующей стандарту, был бы более сложным.
Метод, используемый в этом посте, не зависит от значения, которое имеет CHAR_BIT, но использует это число байтов является степенью числа 2.

ОТРЕДАКТИРОВАНО: я немного изменил метод "поиска", начиная с 1 и затем увеличивая, чтобы избежать возможных "ложных" совпадений с оператором << и большими числами (никогда не знаешь...). Итак, первое совпадение, безусловно, правильное.

person pablo1977    schedule 02.09.2014
comment
Кажется, что все идет в правильном направлении, но я думаю, вы, возможно, упустили из виду, что ваше значение b не обязательно равно CHAR_BIT*p для некоторой степени двойки p или даже некоторого целого числа с. SIZE_MAX из 524287 (2 ^ 19 - 1), вероятно, является законным, и я говорю, вероятно, только потому, что у меня нет копии C99 на этом компьютере. - person zwol; 02.09.2014
comment
Значение b является логарифмом, это число битов, некоторое, это CHAR_BIT * (что-то целое). С другой стороны, я знаю универсальность стандарта C99. Я предполагаю, что можно было бы иметь в практических случаях. В вашем случае количество бит равно 19. У вас действительно есть этот случай в вашей реализации для значения SIZE_MAX? - person pablo1977; 02.09.2014
comment
В любом случае, вероятно, вы можете использовать метод, который я написал, в качестве первого быстрого приближения, а затем продвигаться немного дальше, шаг за шагом, пока не достигнете точного числа битов, скажем, 19. Если бы SIZE_MAX+1 не было степенью 2, это была бы очень экзотическая реализация. - person pablo1977; 02.09.2014
comment
На самом деле у меня нет такой реализации — исходный код отлично работает для всех систем, которые мне интересны на практике, — но это явный вопрос, можете ли вы вообще это сделать, учитывая полную общность того, что разрешено стандартным вопросом. - person zwol; 02.09.2014
comment
Что ж, основная проблема состоит в том, чтобы получить некоторую верхнюю границу искомой величины, которую можно получить, используя мой метод (возможно, с небольшой модификацией). Затем результат можно улучшить, выполнив бинарный поиск по степеням двойки. Возможно, рассмотрение CHAR_BIT вообще было бы снято. - person pablo1977; 02.09.2014

Альтернативой решению sqrt(SIZE_MAX+1) является просмотр цели более высокого уровня, показанной здесь и ниже.

/* s1*s2 <= SIZE_MAX if s1 < K and s2 < K, where K = sqrt(SIZE_MAX+1) */
const size_t MUL_NO_OVERFLOW = ((size_t)1) << (sizeof(size_t) * 4);
if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) &&
    nmemb > 0 && SIZE_MAX / nmemb < size)
  abort();

Простое обнаружение переполнения может использовать следующее. Это, к сожалению, выполняет деление - обширный тест, но в остальном "работает". MUL_NO_OVERFLOW не нужно.

if (nmemb > 0 && (SIZE_MAX / nmemb < size))
  abort();

Вместо деления код OP выполняет 2 простых сравнения с MUL_NO_OVERFLOW, чтобы исключить большинство претендентов.

if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) && ...

Тем не менее, вычисление MUL_NO_OVERFLOW на препроцессоре является сложной задачей, поэтому этот пост.

Альтернативой является использование легко вычисляемого MUL_NO_OVERFLOW_ALT для исключения множества пар size, nmemb из теста на деление.

// good for up to SIZE_MAX + 1 == 2**128
#define SQN(x) ( !!(x) )
#define SQ0(x) ( (x) >= 0x2u ? 1 + SQN((x)/0x2u) : SQN(x) )
#define SQ1(x) ( (x) >= 0x4u ? 2 + SQ0((x)/0x4u) : SQ0(x) )
#define SQ2(x) ( (x) >= 0x10u ? 4 + SQ1((x)/0x10u) : SQ1(x) )
#define SQ3(x) ( (x) >= 0x100u ? 8 + SQ2((x)/0x100u) : SQ2(x) )
#define SQ4(x) ( (x) >= 0x10000u ? 16 + SQ3((x)/0x10000u) : SQ3(x) )
#define SQ5(x) ( (x) >= 0x100000000u ? 32 + SQ4((x)/0x100000000u) : SQ4(x) )
#define SQ6(x) ( (x)/0x100000000u >= 0x100000000u ? 64 + SQ5((x)/0x1000000000000000u/16) : SQ5(x) )
#define MUL_NO_OVERFLOW_ALT (((size_t)1) << (SQ6(SIZE_MAX)/2))

printf("%zx %zx\n", SIZE_MAX, MUL_NO_OVERFLOW_ALT);
// Output
// ffffffff 10000

В случае OP, даже если наилучший (size_t)sqrt(SIZE_MAX + 1.0) был возможен во время компиляции, тест деления все равно потребуется для прохождения допустимых пар size, nmemb, таких как 4, SIZE_MAX/100. Так что для этой задачи лучшего ISQRT(SIZE_MAX + 1.0) не нужно, просто что-то близкое и не большее.

С помощью этого кода наилучшее значение вычисляется, когда SIZE_MAX + 1 является четной степенью числа 2 - распространенный случай.

person chux - Reinstate Monica    schedule 16.10.2017

Следующее работает, но нуждается в очистке.

Предполагает:
1) SIZE_MAX + 1 - это некоторая степень числа 2, нечетная или четная.
2) unsigned long long диапазон не менее size_t.
3) sizeof (SIZE_MAX) ‹= 128 бит.

Примечания:
1) SIZE_MAX должно быть не менее 65535.
2) Слишком много ()
3) Требуется дополнительное тестирование.

Метод:
Найдите степень числа 2 числа SIZE_MAX/4 + 1 с помощью вложенных определений. Нужен другой уровень для каждого двойного бита. На каждом уровне разрядности 32, 64, 128, ... тестируются старшие биты SIZE_MAX. Как только будет найдена максимальная разрядность в степени 2, выполните рекурсию вниз, чтобы получить бит с SIZE_MAX.

Сформируйте sqrt(SIZE_MAX + 1) из (1 << Bit_Width(SIZE_MAX/4 + 1)/2)*2. Умножьте на масштабированное sqrt(2), если разрядность была нечетной.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <limits.h>

int main(void) {
  // Swap 1 for various powers of 2 like 256, 512. etc. to test
#define M  SIZE_MAX/1
  printf("M %zX\n", (size_t) M);
#define N (M/4 + 1)
  printf("N/4 + 1 %zX\n", (size_t) N);

// Obviously this line can be simplified.
#define M0(x)  ((x & ~0x0U) ? 0 : 0)

#define M1(x)  ((x & ~0x1U) ? 1 + M0(x >> 2) : M0(x))

#define M2(x)  ((x & ~0x3U) ? 2 + M1(x >> 2) : M1(x))

#define M3(x)  ((x & ~0xFU) ? 4 + M2(x >> 4) : M2(x))

#define M4(x)  ((x & ~0xFFU) ? 8 + M3(x >> 8) : M3(x))

#define M5(x)  ((x & ~0xFFFFLLU) ? 16 + M4(x >> 16) : M4(x))

#if (M & ~0xFFFFFFFFLLU)
#define M6(x)  ((x & ~0xFFFFFFFFLLU) ? 32 + M5(x >> 32) : M5(x))
#if (M & ~0xFFFFFFFFFFFFFFFFLLU)
#define MN(x)  ((x & ~0xFFFFFFFFFFFFFFFFLLU) ? 64 + M6(x >> 64) : M6(x))
#define MSQ2(odd, x) ((odd&1) ? (x*13043817825332782212llu)>>63 : x)
#else
#define MN(x)  M6(x)
#define MSQ2(odd, x) ((odd&1) ? (x*3037000499llu)>>31 : x)
#endif
#else
#define MN(x)  M5(x)
#define MSQ2(odd, x) ((odd&1) ? (x*46340llu)>>15 : x)
#endif

  printf("MN(N) %d\n", MN(N));
  printf("MN(M) %d\n", MN(M));

#define MUL_NO_OVERFLOW   ((size_t) MSQ2(MN(N), ((((size_t)1) << (MN(N)/2))*2) ))

  printf("MUL_NO_OVERFLOW %zu\n", MUL_NO_OVERFLOW);
  return 0;
}

Выглядит слишком сложно - посмотрю насчет упрощения. ГТГ

person chux - Reinstate Monica    schedule 03.09.2014