Предисловие

Вопреки тому, что авторских C++ библиотек для длинных целых очень много, мне было трудно найти решение, которое было бы простым в использовании на всех этапах (интеграция зависимости, разработка, релиз с зависимостями). Авторские библиотеки имеют одну или несколько проблем реализации: используют10^nв качестве базы счисления, нужна компиляция исходников, не оттестированные, неполный интерфейс (отсутствуют побитовые операторы), не кроссплатформенные. Что касается известных библиотек, то они далеко не просты в использовании:

  • Boost.Multiprecision - это часть огромного фреймворка. Для больших проектов это отлично, так как в Boost много полезных вещей, но мы далеко не каждый день пишем большие проекты. Кроме того, в Boost длинное целое со знаком хранит несоответствующий размеру диапазон значений, так как у него ещё один бит под знак.

  • GNU MP - это C библиотека, требует отдельной компиляции и нацелена на Unix-подобные системы. Даже с C++ обёрткой её далеко не просто скомпилировать и использовать.

  • Crypto++ (https://github.com/weidai11/cryptopp) - самый простой из трёх, но обязательно надо компилировать исходники, с чем не всегда хочется возиться.

Все эти библиотеки отличные для больших и серьёзных проектов. Но есть и простые ситуации: студент первого или второго курса, который хочет сделать более-менее реальный RSA, хотя бы с точки зрения количества бит; программист разрабатывает свой проект, стартап, или даже коммерческий PoC, где не хотелось бы тратить много времени на добавление внешних библиотек, но в то же время не хочется рисковать и брать что-то не совсем надёжное. Авторские библиотеки очень часто протестированы не самым убедительным способом.

Также, в Java, C# и Python длинное целое доступно "из коробки". Всё это подтолкнуло меня к написанию собственной реализации, с надеждой, что в будущем она кому-то будет полезной.

О библиотеке

Arbitrary Precision размещена на GitHub: https://github.com/arbitrary-precision/ap.
В будущем планируется поддерживать длинные числа с плавающей запятой, предлагать более богатый функционал, при этом не изменяя фундаментальному качеству - простота на всех этапах.

Arbitrary Precision как библиотека обладает такими качествами:

  • Header-only (по умолчанию, с возможностью включить опцию компиляции).

  • Кроссплатформенность (GCC, MSVC, не должно быть проблем и с другими).

  • Совместима с разными стандартами (C++11, C++14, C++17).

  • Не генерирует предупреждений.

  • Тщательно протестирована.

  • Интуитивная.

Версия 1.2.0, актуальная на момент написания статьи, предлагает типы для знакового и беззнакового длинного целого фиксированного размера со следующим интерфейсом:

  • Конструкторы копирования и перемещения. Перегружены для свободного преобразования между разными экземплярами класса длинного целого.

  • Все операторы и конструкторы перегружены для работы с базовыми целыми типами.

  • Конструктор, методы и операторы для инициализации из std::string, const char*. Преобразование вstd::string с поддержкой базы до 16.

  • Все бинарные арифметические и побитовые операторы (кроме сдвига), их варианты с присваиванием и операторы сравнения перегружены для всех базовых целых типов и для всех экземпляров класса длинного целого.

  • Операторы сдвига в качестве правого операнда принимают только базовые целые типы (пока что).

  • Все унарные операторы (~, +, -, ++, --).

  • Операторы ввода и вывода с распознаванием флагов базы для вывода.

Использование

Добавление библиотеки в проект элементарное. Нужно скачать исходный код и подключить заголовок ap.hpp (где бы он не находился, обычно это будет <ap/ap.hpp>). В глобальном пространстве имён доступно два шаблона: знаковый ap_int<size_t> и беззнаковый ap_uint<size_t>, которые в качестве параметра принимают размер типа в битах.

Нет смысла детально расписывать и демонстрировать использование операторов сравнения, арифметических и побитовых операторов во всевозможных ситуациях. Эти типы ведут себя точно так же, как и обычный int и unsigned int. Краткий пример:

#include <iostream>
#include <ap/ap.hpp>

ap_int<256> fact(ap_uint<256> val)
{
    ap_int<256> result = 1; // Инициализация копированием.
    for (ap_int<128> i = 1; i <= val; ++i) // Инкремент, сравнение разных типов.
    {
        result *= i; // Умножение разных типов.
    }
    return result;
}

int main()
{
    std::cout << fact(30) << std::endl; // Неявно, конструирование с базовых типов разрешено
    std::cout << fact(ap_int<128>(30)) << std::endl;  // Неявно, тип меньше размером.
    std::cout << fact(ap_uint<256>(30)) << std::endl; // Тот же тип.
    std::cout << fact(ap_int<256>(30)) << std::endl;  // Неявно, та же ширина.
    // std::cout << fact(ap_uint<512>(30)) << std::endl; // Ошибка, необходимо явно привести тип.     
    return 0;
}

Список важных моментов, которые следует помнить при использовании:

  • Знаковые числа ведут себя так, будто они представлены в дополнительном коде (изнутри это не так). Диапазон значений соответствует размеру, указанному в качестве параметра шаблона.

  • Деление на ноль задействует обычный сценарий (просто делит на ноль).

  • Сдвиги арифметические. Если сдвиг происходит на количество битов, большее, чем размер типа, то результат 0. Если это была операция сдвига вправо и число было отрицательным, то результат -1.

  • Размер типа в битах должен быть строго больше unsigned long long.

  • Размер типа в битах должен быть кратен размеру AP_WORD - типа, который используется для хранения данных (unsigned int по умолчанию).

Более интересным является взаимодействие со строками. Основным методом класса для преобразования из строки является set:

integer& set(const char* str, index_t size = 0, index_t base = 0, const char* digits = AP_DEFAULT_STR);       
// str    - строка, представляющая значение.
// size   - размер строки. По умолчанию определяется при помощи strlen.
// base   - база числа, используемая строкой. По умолчанию определяется автоматически.
// digits - система знаков для отображения значения. По умолчанию "0123456789ABCDEF".

Этот метод вызывается другими вариантами:

// set() для std::string.
integer& set(const std::string& str, index_t base = 0, const char* digits = AP_DEFAULT_STR);
// Конструкторы.
explicit integer(const std::string& str, index_t base = 0, const char* digits = AP_DEFAULT_STR);
explicit integer(const char* str, index_t size = 0, index_t base = 0, const char* digits = AP_DEFAULT_STR);         
// Операторы присваивания.
integer& operator=(const std::string& str)
integer& operator=(const char* str)

Чтобы преобразовать в строку, есть два способа:

// Гибкий вариант.
std::string str(index_t base = AP_DEFAULT_STR_BASE, const char* digits = AP_DEFAULT_STR) const;        
// base   - база, в которую надо преобразовать. По умолчанию 10.
// digits - набор символ, который нужно использовать. По умолчанию "0123456789ABCDEF".

// Преобразовывает в десятичное число.
explicit operator std::string() const;

Пример использования:

#include <iostream>
#include "ap/ap.hpp"

int main()
{
    ap_int<128> a{"0b1"}; // Тривиальное преобразование, автоматическое определение базы. 
    ap_int<128> b{std::string("-Z"), 3, "XYZ"}; // Собственная система знаков.
    ap_int<128> c;
    c = "3"; // Присваивание.
    ap_int<128> d; 
    d.set("-1009736", 4, 2); // Явное указание размера строки 4 и базы 2. Т.е. "-100", или же -4.                      
    
    // Десятичное представление, 1 -2 3 -4:
    std::cout << std::string(a) << ' ' << std::string(b) << ' ' << std::string(c) << ' ' << std::string(d) << '\n';
    // Двоичное представление, 0b1 -0b10 0b11 -0b100:
    std::cout << a.str(2) << ' ' << b.str(2) << ' ' << c.str(2) << ' ' << d.str(2) << '\n';
    // Собственное представление в троичной системе, Y -Z YX -YY:
    std::cout << a.str(3, "XYZ") << ' ' << b.str(3, "XYZ") << ' ' << c.str(3, "XYZ") << ' ' << d.str(3, "XYZ") << '\n';         
}

Нюансы:

  • Вне зависимости от базы, преобразованная строка имеет представление "знак и модуль".

  • Если при преобразовании в строку указана база 2, 8 или 16, то всегда добавляется префикс.

Также перегружены операторы ввода-вывода. Тут ничего особенного. Ввод - чтение строки и преобразование из нее. Вывод - преобразование в строку, если установлены флаги std::ios_base::oct или std::ios_base::hex то будет использована соответствующая база.

Режим компиляции исходников

Если определить макрос AP_USE_SOURCES, то .cpp файлы должны быть скомпилированы отдельно. Это опция для тех, кто предпочитает компиляцию, а не header-only подход. Если макрос AP_USE_SOURCES не определён, а .cpp всё равно компилируют, то эту ситуацию обработает compile guard (на примере asm.*):

// asm.hpp
#ifndef DEOHAYER_AP_ASM_HPP
#define DEOHAYER_AP_ASM_HPP

// ...
// Объявления функций.
// ...

// Если .cpp не компилируются - включить их в .hpp.
#ifndef AP_USE_SOURCES
#define DEOHAYER_AP_ASM_CPP 0 // Для совместимости с compile guard.
#include "asm.cpp"
#endif

#endif
// asm.cpp
#ifndef DEOHAYER_AP_ASM_CPP
#define DEOHAYER_AP_ASM_CPP 1 // Случай компиляции.
#endif
// Код не будет проигнорирован только если значения совпадают.
// .hpp устанавливает макрос в 0, поэтому код попадёт в .hpp файл.
// .cpp устанавливает макрос в 1, а при случайной компиляции AP_USE_SOURCES не определён.
// код после if для компилируемой версии .cpp файла в таком случае просто будет отброшен.      
#if (DEOHAYER_AP_ASM_CPP == 1) == defined(AP_USE_SOURCES)

// ...
// Определения функций.
// ...

#endif

Быстродействие

Данные замерялись простым способом. Для указанного размера целого числа генерируется левый операнд, где значение занимает 25-50% размера и правый операнд, где значение занимает 12-25% размера. Дальше выполняется операция, и замеряется время исполнения для AP и Boost. В таблице указано соотношение AP/Boost.

Версия компилятора GCC: Ubuntu 7.5.0-3ubuntu1~18.04:

Размер

+

-

*

/

%

<<

>>

128

1.74487

2.23426

2.43082

6.32478

5.87538

2.17034

1.6978

512

1.23195

1.43016

1.16948

0.862215

0.96964

1.43523

1.63936

4096

0.960102

1.12024

0.980719

0.444539

0.487134

1.21475

1.38079

10240

1.41084

1.23715

0.933805

0.380653

0.408858

1.32783

1.36085

AP несущественно проигрывает Boost на больших значениях (1.1-1.5 раза), деление же намного лучше. Малые значения пока что не оптимизированы, из-за этого такая заметная разница. (2-6 раз).

Примечание: для замера использовалась программа https://github.com/arbitrary-precision/ws/blob/master/src/measure/measure.cpp. Из-за того, что Boost использует __int128 при компиляции с GCC, пришлось сконфигурировать AP, чтобы она тоже использовала этот тип (корневой CMakeLists.txt):

add_executable(measure ${MEASURE_SRC_FILES})
target_compile_options(measure PUBLIC
  -DAP_WORD=unsigned\ long\ long
  -DAP_DWORD=unsigned\ __int128
) 

__int128 не полностью поддерживается сейчас - он ломает взаимодействие с базовыми типами. На операции с длинными числами это не влияет, они работают корректно.

Внутреннее устройство

Вся библиотека базируется на трёх типах:

  • word_t, или слово. Аналог limb-ов в GNU MP и Boost, служит для хранения данных. Этот тип можно задать через макрос AP_WORD.

  • dword_t, или двойное слово. Аналог limb в GNU MP и Boost, служит для выполнения операций и хранения промежуточных значений. Этот тип можно задать через макрос AP_DWORD, размер должен быть ровно в два раза больше, чем размер word_t.

  • index_t, аналог size_t.

Можно выделить пять основных сущностей библиотеки:

  • Шаблон integer<size_t Size, bool Sign> (как контейнер значения, integer.hpp) - его частичными специализациями являются ap_int<size_t> и ap_uint<size_t>.

  • dregister<T> (core.hpp), или "регистр" - POD тип, единое легковесное представление длинного целого. Хранит указатель на массив слов (word_t), T и есть типом указателя - const word_t* или word_t* в зависимости от того, разрешена ли запись значения. Остальные поля - размер массива, размер значения (сколько слов задействовано в данный момент), и знак.

  • Внешние функции (integer.hpp) - все методы integer<size_t, bool> и все внешние операторы, перегруженные для него. Работают на только на уровне класса integer<size_t, bool>. Ответственность - обеспечение взаимодействия всевозможных типов.

  • Внутренние функции (integer_api.*) - набор функций, которые использует integer в своих методах. Работают на уровне dregister<T>. Разделены на две группы - для знаковых и беззнаковых, не накладывают никаких ограничений на операнды. Ответственность - подготовка операндов для вызова алгоритмических функций, корректная расстановка знаков, нормализация после выполнения алгоритма (о ней чуть ниже).

  • Алгоритмические функции (asm.*) - являются чистым алгоритмом, работают с dregister<T>, но интерпретируют его исключительно как беззнаковое целое. Кроме того, они накладывают строгие ограничения на операнды и могут иметь очень специфическое поведение. Арифметические алгоритмы - это алгоритмы A, S, M, D из "Искусство программирования (Том 2)" Д. Кнута. Остальные написаны самостоятельно. Ответственность - выполнение операции, получение корректного с точки зрения беззнакового числа битового паттерна.

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

  • Размер должен быть установлен таким образом, что word_t с индексом [размер - 1] не равен нулю.

  • Знак при размере 0 должен быть 0.

  • (только знаковое) Арифметическое значение, представляемое dregister<T> числа не должно выходить за рамки диапазона [-2^n,  2^n - 1], где n - битовый размер числа.

С первыми двумя пунктами всё вполне прозрачно: "-0" это недопустимое значение, а нули в начале старших разрядов ломают алгоритм деления и оперировать нулями не имеет смысла. Третий пункт обеспечивает то, что знаковое ведёт себя так, будто оно представлено в дополнительном коде. После выполнения операции проверяется старший бит массива слов. Если он 1, то весь массив перекидывается в его дополнительный код и числу выставляется знак "-". Это работает даже если ненулевым является исключительно старший бит (дополнительный код такого паттерна это он сам). После этого выполняется нормализация по первым двум пунктам.

Тестирование

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

В качестве эталонной библиотеки выбрана Boost.Multiprecision. Единое строковое представление - "знак и модуль" в шестнадцатеричной системе. Целое число в тестировании характеризуется следующими параметрами:

  • Битовый размер. Большой (L, 512 бит) или малый (S, 256 бит).

  • Размер (данных). Одно слово, 25%, 50%, 75%. 100% от битового размера (это всегда целое количество слов).

  • Знаковость. Знаковое или беззнаковое.

  • Битовый паттерн. Определено 10:

Все нули

00000000 00000000 ... 00000000 00000000

Все единицы

11111111 11111111 ... 11111111 11111111

Малый шахматный 0

01010101 01010101 ... 01010101 01010101

Малый шахматный 1

10101010 10101010 ... 10101010 10101010

Большой шахматный 0

00000000 11111111 ... 00000000 11111111

Большой шахматный 1

11111111 00000000 ... 11111111 00000000

Только младший

00000000 00000000 ... 00000000 00000001

Только старший

10000000 00000000 ... 00000000 00000000

Все, кроме младшего

11111111 11111111 ... 11111111 11111110

Все, кроме старшего

01111111 11111111 ... 11111111 11111111

Всего отдельных комбинаций для абстрактной бинарной операции 80000:

  • 8 комбинаций битовых размеров (левый операнд, правый операнд, результат) - LLL, LLS, LSL, LSS, SLL, SLS, SSL, SSS. Считается, что все три аргумента передаются независимо друг от друга.

  • 25 комбинаций размеров (5 левый, 5 правый).

  • 4 комбинации знаковости операндов (в отличие от битового размера, зависит от операндов).

  • 100 комбинаций битовых паттернов (10 левый, 10 правый).

Эти тесты запускаются и для внешних, и для внутренних функций отдельно. Некоторые сценарии не релевантны, они отсеяны. Внешние функции никогда не могут иметь сценарий битовых размеров LLS или SSL (int + int != long long int), а внутренние не имеют функций, которые бы работали с операндами разной знаковости. Т.е. количество тестов 60000 и 40000 соответственно.

Отдельная проверка выглядит следующим образом:

  1. Сгенерировать операнды заданного размера относительно заданного битового размера.

  2. Инициализировать целые Boost, результат конвертировать в беззнаковою строку (т.е дополнительный код). Эту строку подогнать под заданные размер и знаковость третьего аргумента (хранящего результат).

  3. Инициализировать операнды ap (для внешних функций это integer<size_t, bool>, для внутренних - dregister<T>. Затем выполнить операцию и просто конвертировать результат в строку, не выполняя никаких других превращений.

  4. Сравнить полученные строки, они должны быть идентичны.

Реализовано это в репозитории ws, который был н енапрямую упомянут в "Быстродействие": https://github.com/arbitrary-precision/ws (подпроект src/validate). Детали реализации в этой статье приводиться не будут, это довольно большой объём информации.

Что в планах

  • Написать тесты для взаимодействия с базовыми типами.

  • Разобраться с C++20 и SpaceX оператором.

  • Оптимизация унарных операторов.

  • Написать специализацию integer<size_t, bool> для битового размера равному unsigned long long и меньше.

  • Учесть существование __int128 и возможность существования ещё больших intX.

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

  • Оптимизация работы с базовым типом.

  • Оптимизация алгоритмов.

  • Улучшить взаимодействие между signed и unsigned (избегать приведения типа).

  • Расширить интерфейс дополнительными базовыми алгоритмами (sqrt, lcm, gcd, pow etc.).

  • (floating<>).

  • (Ассемблерная оптимизация).

Комментарии (15)


  1. Videoman
    15.12.2021 00:23
    +2

    Посмотрите slimcpplib (Simple long integer math library for C++). По всем вашим требованиям подходит:

    • header-only
    • нет зависимостей
    • может использовать интринсики и работать полностью во время компиляции
    • кроссплатформенная: MSVC, GCC and CLANG C++17 compilers
    • не использует исключения

    Что касается интерфейса, то на вкус и цвет всем не угодишь конечно, но она пытается максимально быть похожей на стандартные целые типы, т.е. в шаблонном коде с генерализацией никаких проблем не будет.
    С полностью constexpr есть проблемы под С++17, так как нет возможности во время компиляции выбирать перегрузки intrinsics/constexpr, поэтому для работы полностью в режиме компиляции приходится жертвовать интринсиками.


    1. Deohayer Автор
      15.12.2021 02:32

      Спасибо, надо будет посмотреть. Постоянно сравнивать со всевозможными конкурентами неплохо так съедает время. Похоже, нужно написать фреймворк для анализа библиотеки такого типа, т. е. учитывать корректность работы, быстродействие и наличие конкретных элементов интерфейса (операторов и конструкторов). Всё равно хотел что-то подобное сделать, чтобы иметь возможность оценивать прогресс между версиями.

      В slimcpplib использование std::array настораживает. И из-за этого интересует быстродействие.

      Header-only это хорошо и удобно, все счастливы. "А сколько весит билд и длится компиляция?". После этих слов в программистском поезде начинается сущий кошмар.

      constexpr - интересно, хорошо, что есть. По крайней мере, можно посчитать константы. Если они часто используются, то это неплохая оптимизация. То же решето Эратосфена - сохранить столько, сколько можно.

      Если брать откровенную вкусовщину без анализа, то:

      1. Как сделать динамический инт (без указывания размера). У меня тоже нет, и мне на это указывали.

      2. std::array подразумевает много копирования.

      3. Нет побитовых операторов для знакового. "Не решайте за меня, что мне не нужно".

      4. Нет интерфейса для взаимодействия с базовым целым (если я не пропустил).

      5. Только С++17. А 11, 14 и 17 одинаково популярны.

      6. Почему только header-only.


      1. Deosis
        15.12.2021 08:07

        Плюс std::array в том, что его можно разместить целиком на стеке. У вас же приходится тратиться на указатели.


        1. Deohayer Автор
          15.12.2021 11:18

          С ним ещё sizeof() правильно работать будет, тоже важно.


      1. Videoman
        15.12.2021 12:20
        +1

        Похоже, нужно написать фреймворк для анализа библиотеки такого типа, т. е. учитывать корректность работы, быстродействие и наличие конкретных элементов интерфейса (операторов и конструкторов).
        Да, было бы неплохо все мерить в одних и тех же попугаях.
        В slimcpplib использование std::array настораживает. И из-за этого интересует быстродействие.
        Я смотрел на ассемблерный выхлоп и не увидел там каких-то артефактов от массивов. Всё раскладывается по регистрам. Скорость, во всяком случае для моих задач, на моей слабенькой машинке Intel® Core(TM) i5-6600 CPU 3.30 GHz, вполне приемлемая. Я смотрел для 128 битных беззнаковых целых:
        • сложение, вычитание, битовые операции — доли наносекунд
        • умножение — единицы наносекунд
        • деление — десятки наносекунд
        Как сделать динамический инт (без указывания размера). У меня тоже нет, и мне на это указывали.
        Тут надо определится для чего мы делаем библиотеку. Это не формальные определения, но тем не менее: есть библиотеки типа big integer, для огромных целых, размер которых определяется в процессе вычислений и подстраивается под задачу, а есть библиотеки типа long integer, для вычислений немного превышающих нативный тип. Оптимизации и алгоритмы и там и там обычно под разные цели.
        Если мы позволим размеру расти динамически, то мы потеряем его как константу времени компиляции, что безусловно скажется на оптимизации.
        Нет побитовых операторов для знакового. «Не решайте за меня, что мне не нужно».
        Ну тут проблема тогда: как мы определяем что такое сдвиг знакового числа влево или вправо, нужно ли нам расширять знак или нет? В принципе конечно можно сделать так как это теперь определяет стандарт С++20.
        Нет интерфейса для взаимодействия с базовым целым
        А что под этим подразумевается?
        Только С++17
        Это из-за того, что только в С++17 завезли нормальные литералы с которыми можно реализовать человеческий интерфейс, а также расширили правила с constexpr так, чтобы всё это можно было реализовать


        1. Deohayer Автор
          15.12.2021 14:47

          Я смотрел для 128 битных беззнаковых целых

          Не показатель. В Boost это просто использование __int128. slimcpplib тоже знает об этом типе. Надо смотреть что-то более показательное, хотя бы 4096, и запустить несколько тысяч раз хотя бы. Я этим займусь через пару недель :)

          Тут надо определится для чего мы делаем библиотеку.

          Всё просто: нам мало int, который даёт C++, мы дописываем свой, побольше, но так, чтобы было не отличить от настоящего, маленького. И в нём должно быть что-то для каждого. Всё, для чего есть несколько точек зрения, должно быть настраиваемым. Исключение: представление знаковых. Тут мимикрия под дополнительный код и арифметический сдвиг. Во-первых, это распространено, C++20 тому подтверждение (наконец-то!). Во-вторых, настраиваемость представления и поведения знаковых банально усложнит поддержку и тестирование, и всё ради опции, которая почти никому не нужна.

          Должны быть и фиксированные, и свободно расширяющиеся инты. И стековая и динамическая память. И со знаком, и без знака. И с исключениями при переполнениях и делении на ноль, и без. И всё в таком духе дальше.

          А что под этим (взаимодействие с базовым целым) подразумевается?

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

          Это из-за того, что только в С++17 завезли нормальные литералы с которыми можно реализовать человеческий интерфейс

          Тут не constexpr главный, а std::array, потому что с динамическим массивом это не работает. Но даже это можно добавить, сначала нужно реализовать большое целое на стеке, а дальше макросы наше всё: ap_linkage, ap_constexpr...

          Ну и "человеческий интерфейс" - громковато для возможности посчитать константу во время компиляции и задать значение без кавычек. Безусловно интересная, хорошая возможность, есть свои области применения, но и без неё интерфейс останется человеческим.

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


          1. Videoman
            15.12.2021 15:56

            В Boost это просто использование __int128. slimcpplib тоже знает об этом типе. Надо смотреть что-то более показательное, хотя бы 4096, и запустить несколько тысяч раз хотя бы. Я этим займусь через пару недель :)
            Отлично, будет очень интересно сравнить. Только нужно всегда помнить, что __int128 отсутствует в 32-х битной конфигурации, а также не поддерживается в компиляторе от Microsoft, до VS2019, что затрудняет написание обобщенного кода. Я, например, сейчас активно использую компиляцию одновременно и в 32-х битный и в 64-х битный код.
            Должны быть и фиксированные, и свободно расширяющиеся инты. И стековая и динамическая память. И со знаком, и без знака. И с исключениями при переполнениях и делении на ноль, и без. И всё в таком духе дальше.
            После определенной границы длины чисел, активно начинает работать другая асимптотика и другие алгоритмы, другие пропорции для K и для O(). Это также нужно учитывать.
            Операторы и конструкторы большого целого должны спокойно работать с обычными целыми, без явного приведения типа снаружи (да и внутри, желательно, ради скорости).
            С этим у slimcpplib все в порядке. Единственное, она запрещает неявные сужающие преобразования от большего значения к меньшему, только через static_cast.
            Но мне нужен ранний фидбек, чтобы понять, куда двигаться дальше и что учесть при дальнейшей разработке.
            Не, я ни в коем случае не отговариваю вас от задумки. Планы действительно грандиозные и будет очень здорово, если всё это удастся совместить в одной библиотеке с float-ами и вот этим вот всем. Главное не увлечься так, что бы не потерять в легкости библиотеки, её скорости и простоте. Удачи! Больше хороших и нужных библиотек.


            1. Deohayer Автор
              15.12.2021 16:24

              что затрудняет написание обобщенного кода

              Условная компиляция и вперёд. Не так красиво, как одна функция на все случаи жизни, конечно. Но гибкость важнее.

              После определенной границы длины чисел, активно начинает работать другая асимптотика и другие алгоритмы, другие пропорции для K и для O(). Это также нужно учитывать.

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

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

              Ну тогда всё отлично, корректное поведение.

              Удачи! Больше хороших и нужных библиотек.

              Спасибо!


    1. myrrc
      16.12.2021 12:56

      Для этих случаев есть std::is_constant_evaluated (C++20), в C++23 добавят if consteval.


  1. Ingulf
    17.12.2021 08:14

    1. как насчет интеграции с системами сборки, например тем же CMake из коробки?

    2. отмечается плохое покрытие тестами у авторских библиотек, как с этим обстоят дела? как я понял из-за сложности так же покрытие не полное? кроме того тесты отдельным проектом ну такое

    3. не очень заметил какую-либо документацию


    1. Deohayer Автор
      17.12.2021 10:23

      1. Нет интеграций со системами сборки, так как интеграция тривиальна - нужно скачать репозиторий и добавить его в проект. "Добавить в проект" - расположить папку там, где хочется, опционально добавив её в include path. Если хочется компиляции - всё то же самое, плюс глобнуть все .cpp файлы, что делается одной строчкой в GNU Make / CMake. Для Visual Studio - пара кликов, он сама знает, что делать с .cpp.

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

      3. Тут оправдываться не могу. Документацию надо будет сделать.


  1. Kelbon
    17.12.2021 09:54

    Простите что я тут вижу вообще? Это С?

    По описанию "проблем" с другими библиотеками и описанию достоинств своей выглядит, что вы не совсем разбираетесь в плюсах, например почему компиляция из исходников это проблема? Почему поддержка gcc и msvc это кросплатформенность?

    • Header-only (по умолчанию, с возможностью включить опцию компиляции).

    Что это вообще значит? Опция чтобы компилировалось? Что насчет поддержки С++20? Почему так явно перечислены 11 14 17?


    1. Deohayer Автор
      17.12.2021 11:06

      Простите что я тут вижу вообще? Это С?

      Нет, это оптимизация работы с памятью!) Всё ради realloc. В будущем будет полезно, так как будут ещё и саморасширяющиеся типы (грубо говоря бесконечные), о них много вопросов было.

      А сейчас, например, это позволяет оптимизировать операции перемещения (move) между целыми разных размеров:

      ap_int<512> a = ap_int<1024>(5);

      Вместо аллоцирования через new (make_unique) и копирования, просто переставляется указатель, а размер массива урезается при помощи realloc. Ну и если наоборот, слева размер больше, справа - меньше, то есть неплохой шанс, что realloc увеличит массив без копирования. И как приятный бонус: если копирование произошло, мне не надо делать никаких дополнительных вызовов или циклов для копирования, realloc всё сделал за меня.

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

      например почему компиляция из исходников это проблема?

      Для header-only надо один include, для компиляции надо этот же include, плюс что-то компилировать. Даже если будет CMake, GNU Make, любая автоматизация этого процесса - надо будет ещё где-то тыкануть строку "вызови вот это вот". И прочитать о том, что нужно тыкануть эту строку. Для меня это минус - я ленивый, и хочу дать возможность таким же ленивым людям не делать лишних телодвижений при работе с библиотекой. При этом я не хочу обижать тех, кто знает о недостатках header-only подхода. Вот почему компиляция не просто "есть или нет" - она опциональна.

      Почему поддержка gcc и msvc это кросплатформенность?

      Контекстуально. Первой - популярный компилятор на Linux, второй - на Windows. "Я проверил компиляцию на двух ОС и сделал это вот такими компиляторами". Нет, кроме этих ни на чём другом не пробовал, признаюсь сразу. Обязательно попробую.

      Что это вообще значит? Опция чтобы компилировалось?

      Да, об этом отдельный пункт есть "Режим компиляции исходников". Если задефайнить AP_USE_SOURCES то нужно будет скомпилировать все .cpp файлы библиотеки, без каких-либо нюансов.

      Что насчет поддержки С++20? Почему так явно перечислены 11 14 17?

      Потому что я не проверял поддержку С++20 и, скорее всего, не работает. Упомянуто в пункте "Что в планах".


      1. Kelbon
        17.12.2021 11:45

        Что будет если realloc вернёт ошибку? И ведь его ошибка не такая, что случится только если вся память завалена как у обычного выделения памяти.

        Где поддержка аллокаторов тогда?(это не аллокатор)

        Потеря производительности при обращении по указателю каждый раз?

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

        А вы экономите на memcpy 2-10 байт теряя на обращении через поинтер каждый раз и ломаете семантику поведения, ваши числа ведут себя не также как обычные инты

        Кстати, realloc в С++ это вообще по умолчанию УБ, т.к. на компиляции неизвестно что делать с лайфтаймами объектов


        1. Deohayer Автор
          17.12.2021 13:16
          +1

          Что будет если realloc вернёт ошибку

          Обработку такой ситуации можно добавить. Хотя если такое произошло, то у вас в системе явно проблемы посерьёзнее.

          Где поддержка аллокаторов тогда?(это не аллокатор)

          Возможно будет, возможно нет. Зависит от того, насколько это будет нужно.
          Таких "Где" для библиотек данного типа можно бесконечно придумать - по одному на каждый существующий юзкейс и алгоритм.

          Потеря производительности при обращении по указателю

          Если это проблема - то надо использовать GNU MP и больше ничего. Желательно вообще ничего не использовать, а прямо на ассемблере писать. И я сейчас без сарказма, вполне могу представить себе RTOS или что-то подобное, где частое обращение по указателю было бы проблемой.

          но все кто мувает число считает, что в старом значении валидное опять же число. А вы экономите на memcpy 2-10 байт теряя на обращении через поинтер каждый раз и ломаете семантику поведения, ваши числа ведут себя не также как обычные инты

          А ещё оператор sizeof() у меня неправильно возвращает количество байт :)
          Экономлю 4096 / 8 = 512 байт (RSA). Ну, около того.
          Если после std::move к xvalue переменной всё ещё можно обращаться для чтения значения, то это копирование, а не перемещение, вне зависимости от того, базовый тип это или объект. Если я неправ, то скиньте, пожалуйста, где в стандарте это сказано (любой, от 11 до 20). Возможно, я пропустил.

          Кстати, realloc в С++ это вообще по умолчанию УБ, т.к. на компиляции неизвестно что делать с лайфтаймами объектов

          Я эти функции использую для выделения памяти под массив базовых типов, POD. В стандарте указано, что функция стандартной же библиотеки - UB?