Большие целые числа и C

Я пишу программу, которая генерирует большие целые числа, сохраняет их в массиве и выполняет некоторые основные операции, такие как умножение или сложение.

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

Ниже я добавлю фрагмент кода, чтобы вы могли увидеть структуры, которые я использую, и то, как я пытаюсь справиться с этим B.I.N.:

unsigned int seed;


void initCharArray( char *L, unsigned N )
{
  for ( int i=0; i< N; i++ ) 
  {
    L[i] = i%50;
  }
}


char Addition( char *Vin1, char *Vin2, char *Vout, unsigned N )
{
  char CARRY = 0;
  for ( int i=0; i< N; i++ ) 
  {
    char R = Vin1[i] + Vin2[i] + CARRY;
    if ( R <= 9 )
    {
      Vout[i] = R; CARRY = 0;
    }
    else
    {
      Vout[i] = R-10; CARRY = 1;
    }
  }
  return CARRY;
}



int main(int argc, char **argv)
{
int N=10000;
unsigned char *V1=new int[N];
unsigned char *V2=new int[N];
unsigned char *V3=new int[N];

initCharArray(V1,N); initCharArray(V2,N);

Addition(V1,V2,V3,N);
}

person Jonathan Sánchez    schedule 06.04.2020    source источник
comment
Используйте большее основание, например, основание 2^64 вместо основания 10, где каждая цифра представляет собой что-то вроде uint64_t. Если числа, с которыми вы имеете дело, действительно велики, вы можете использовать БПФ для их умножения.   -  person BessieTheCookie    schedule 06.04.2020
comment
@BessieTheCow Числа с основанием 10 обязательны для этого проекта, тем не менее, спасибо за этот совет. Я попробую этот метод БПФ, поскольку другой пользователь также рекомендовал достичь этой сложности (nlog (n)). У вас есть какая-нибудь веб-страница, где я могу прочитать информацию для новичков об этом? Большое спасибо за ваше время.   -  person Jonathan Sánchez    schedule 06.04.2020
comment
Эффективнее всего использовать конечность, равную размеру регистра, то есть основание 2³² или 2⁶⁴ на 32/64-битных компьютерах. См. Зачем использовать более высокую базу для реализации BigInt?, Как очень большие числа с плавающей запятой представлены в памяти?. Каждая хорошая библиотека, такая как GMP или Boost.Multiprecision, уже была очень эффективно написана с использованием базы 2³² или 2⁶⁴ вместо базы 10.   -  person phuclv    schedule 06.04.2020
comment
БПФ работает быстрее только для действительно больших чисел, а также очень продвинут, поэтому я определенно не рекомендую пробовать его в школьном проекте, если вы никогда раньше не сталкивались с подобными алгоритмами и математикой. Если вы действительно хотите изучить БПФ (опять же, вам, скорее всего, не следует делать это на данном этапе), вы можете попробовать просмотреть страницу Википедии и поискать другие учебные пособия в Интернете, но лучшее объяснение, которое я знаю, взято из книги CLRS. .   -  person BessieTheCookie    schedule 06.04.2020


Ответы (1)


Поскольку современные владельцы очень эффективны при работе с числами фиксированной длины в битах, почему бы вам не иметь их массив?

Предположим, вы используете unsigned long long. Они должны иметь ширину 64 бита, поэтому максимально возможное unsigned long long должно быть 2^64 - 1. Давайте представим любое число в виде набора чисел следующим образом:

-big_num = ( n_s, n_0, n_1, ...)

-n_s будет принимать только 0 и 1 для представления знака + и -

-n_0 будет представлять число от 0 до 10 ^ a -1 (показатель степени a будет определяться)

-n_1 будет представлять число от 10^a до 10^(a+1) -1 и так далее, и так далее...

ОПРЕДЕЛЕНИЕ:

Все n_ ДОЛЖНЫ быть ограничены 10^a-1. Таким образом, при добавлении двух big_num это означает, что нам нужно добавить n_ следующим образом:

// A + B = ( (to be determent later),
//          bound(n_A_1 + n_B_1) and carry to next,
//          bound(n_A_2 + n_B_2 + carry) and carry to next,
//        ...)

Ограничение может быть выполнено как:

bound(n_A_i + n_B_i + carry) = (n_A_i + n_B_i + carry)%(10^a)

Следовательно, перенос на i+1 определяется как:

// carry (to be used in i+1) = (n_A_i + n_B_i + carry)/(10^a) 
// (division of unsigned in c++ will floor the result by construction)

Это говорит нам о том, что наихудший случай — carry = 10^a -1, и, таким образом, наихудшее сложение (n_A_i + n_B_i + carry) — это: (worst case) (10^a-1) + (10^a-1) + (10^a-1) = 3*(10^a-1) Поскольку тип unsigned long long, если мы не хотим иметь переполнение при этом сложении, мы должны ограничить нашу экспоненту a так, чтобы:

//    3*(10^a-1) <= 2^64 - 1, and a an positive integer
// => a <= floor( Log10((2^64 - 1)/3 + 1) )
// => a <= 18

Таким образом, теперь зафиксировано максимально возможное значение a=18, и, таким образом, максимально возможное число n_, представленное с помощью unsigned long long, равно 10^18 -1 = 999,999,999,999,999,999. С этой базовой настройкой давайте теперь перейдем к реальному коду. Сейчас я буду использовать std::vector для хранения big_num, о котором мы говорили, но это может измениться:

// Example code with unsigned long long
#include <cstdlib>
#include <vector>
//
// FOR NOW BigNum WILL BE REPRESENTED
// BY std::vector. YOU CAN CHANGE THIS LATTER
// DEPENDING ON WHAT OPTIMIZATIONS YOU WANT
//
using BigNum = std::vector<unsigned long long>;

// suffix ULL garanties number be interpeted as unsigned long long
#define MAX_BASE_10 999999999999999999ULL

// random generate big number
void randomize_BigNum(BigNum &a){
  // assuming MyRandom() returns a random number
  // of type unsigned long long
  for(size_t i=1; i<a.size(); i++)
    a[i] = MyRandom()%(MAX_NUM_BASE_10+1); // cap the numbers
}

// wrapper functions
void add(const BigNum &a, const BigNum &b, BigNum &c); // c = a + b
void add(const BigNum &a, BigNum &b);         // b = a + b

// actual work done here
void add_equal_size(const BigNum &a, const BigNum &b, BigNum &c, size_t &N);
void add_equal_size(const BigNum &a, const BigNum &b, size_t &N);
void blindly_add_one(BigNum &c);
// Missing cases
// void add_equal_size(BigNum &a, BigNum &b, BigNum &c, size_t &Na, size_t &Nb);
// void add_equal_size(BigNum &a, BigNum &b, size_t &Na, size_t &Nb);

int main(){
  size_t n=10;
  BigNum a(n), b(n), c(n);
  randomize_BigNum(a);
  randomize_BigNum(b);
  add(a,b,c);
  return;
}

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

// To do: add support for when size of a,b,c not equal

// c = a + b
void add(const BigNum &a, const BigNum &b, BigNum &c){

  c.resize(std::max(a.size(),b.size()));

  if(a.size()==b.size())
    add_equal_size(a,b,c,a.size());
  else
    // To do: add_unequal_size(a,b,c,a.size(),b.size());

  return;
};
// b = a + b
void add(const BigNum &a, const BigNum &b){

  if(a.size()==b.size())
    add_equal_size(a,b,a.size());
  else{
    b.resize(a.size());
    // To do: add_unequal_size(a,b,a.size());
  }

  return;
};

Основная часть работы будет выполнена здесь (которую вы можете вызвать напрямую и пропустить вызов функции, если знаете, что делаете):

// If a,b,c same size array
// c = a + b
void add_equal_size(const BigNum &a, const BigNum &b, BigNum &c, const size_t &N){
  // start with sign of c is sign of a
  // Specific details follow on whether I need to flip the
  // sign or not
  c[0] = a[0];
  unsigned long long carry=0;

  // DISTINGUISH TWO GRAND CASES:
  //
  // a and b have the same sign
  // a and b have oposite sign
  // no need to check which has which sign (details follow)
  //
  if(a[0]==b[0]){// if a and b have the same sign
    //
    // This means that either +a+b or -a-b=-(a+b)
    // In both cases I just need to add two numbers a and b
    // and I already got the sign of the result c correct form the
    // start
    //
    for(size_t i=1; i<N;i++){
      c[i]  = (a[i] + b[i] + carry)%(MAX_BASE_10+1);
      carry = c[i]/(MAX_BASE_10+1);      
    }
    if(carry){// if carry>0 then I need to extend my array to fit the final carry
      c.resize(N+1);
      c[N]=carry;
    }
  }
  else{// if a and b have opposite sign
    //
    // If I have opposite sign then I am subtracting the
    // numbers. The following is inspired by how
    // you can subtract two numbers with bitwise operations.
    for(size_t i=1; i<N;i++){
      c[i]  = (a[i] + (MAX_BASE_10 - b[i]) + carry)%(MAX_BASE_10+1);
      carry = c[i]/(MAX_BASE_10+1);      
    }
    if(carry){ // I carried? then I got the sign right from the start
      // just add 1 and I am done
      blindly_add_one(c);
    }
    else{ // I didn't carry? then I got the sign wrong from the start
      // flip the sign
      c[0] ^= 1ULL;
      // and take the compliment
      for(size_t i=1; i;<N;i++)
    c[i] = MAX_BASE_10 - c[i];
    }
  }
  return;
};

Ниже приведены некоторые подробности о случае // if a and b have opposite sign: Давайте поработаем с основанием 10. Допустим, мы вычитаем a - b. Давайте преобразуем это в сложение. Определите следующую операцию:

Назовем основание 10 цифр числа di. Тогда любое число будет n = d1 + 10*d2 + 10*10*d3... Дополнение цифры теперь будет определяться как:

     `compliment(d1) = 9-d1`

Тогда комплимент числа n равен:

   compliment(n) =         compliment(d1)
                   +    10*compliment(d2)
                   + 10*10*compliment(d3)
                   ...

Рассмотрим два случая, a>b и a<b:

ПРИМЕР a>b: если не сказать a=830 и b=126. Сделайте следующее 830 - 126 -> 830 + compliment(126) = 830 + 873 = 1703 хорошо, так что если a>b, я опускаю 1 и добавляю 1, результат будет 704!

ПРИМЕР a<b: если не говорить a=126 и b=830. Выполните следующие действия 126 - 830 -> 126 + compliment(830) = 126 + 169 = 295 ...? Ну а если я сделаю комплимент? compliment(295) = 704 !!! так что если a<b у меня уже есть результат... с обратным знаком.

Переходя к нашему случаю, поскольку каждое число в массиве ограничено MAX_BASE_10, дополнение наших чисел равно

compliment(n) = MAX_BASE_10 - n

Таким образом, используя этот комплимент для преобразования вычитания в сложение, мне нужно обратить внимание только на то, есть ли у меня лишняя 1 в конце сложения (случай a>b). Алгоритм теперь такой

  • ДЛЯ КАЖДОГО вычитания МАССИВА (i-я итерация):
  • na_i - nb_i + перенос(i-1)
  • конвертировать -> na_i + комплимент (nb_i) + перенос (i-1)
  • связал результат -> (na_i + комплимент (nb_i) + перенос (i-1))% MAX_BASE_10
  • найти перенос -> (na_i + комплимент (nb_i) + перенос (i-1))/MAX_BASE_10

  • продолжайте добавлять номера массива...

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

Это «и добавить один» выполняется еще одной функцией:

// Just add 1, no matter the sign of c
void blindly_add_one(BigNum &c){
  unsigned long long carry=1;
  for(size_t i=1; i<N;i++){
    c[i]  = carry%(MAX_BASE_10+1);
    carry = c[i]/(MAX_BASE_10+1);
  }
  if(carry){ // if carry>0 then I need to extend my basis to fit the number
    c.resize(N+1);
    c[N]=carry;
  }
};

Хорошо до сюда. Конкретно в этом коде не забываем, что в начале функции мы устанавливаем знак c в знак a. Так что, если я перенесу в конце, это означает, что у меня было |a|>|b|, и я сделал либо +a-b>0, либо -a+b=-(a-b)<0. В любом случае установка знака результатов c на знак a была правильной. Если я не ношу, у меня было |a|<|b| либо с +a-b<0, либо с -a+b=-(a-b)>0. В любом случае установка знака результатов c на знак a была НЕПРАВИЛЬНОЙ, поэтому мне нужно перевернуть знак, если я не ношу.

Следующие функции работают так же, как и предыдущая, только вместо того, чтобы c = a + b делать это, нужно b = a + b

// same logic as above, only b = a + b
void add_equal_size(BigNum &a, BigNum &b, size_t &N){

  unsigned long long carry=0;
  if(a[0]==b[0]){// if a and b have the same sign
    for(size_t i=1; i<N;i++){
      b[i]  = (a[i] + b[i] + carry)%(MAX_BASE_10+1);
      carry = b[i]/(MAX_BASE_10+1);      
    }
    if(carry){// if carry>0 then I need to extend my basis to fit the number
      b.resize(N+1);
      b[N]=carry;
    }
  }
  else{ // if a and b have oposite sign
    b[0] = a[0];
    for(size_t i=1; i<N;i++){
      b[i]  = (a[i] + (MAX_BASE_10 - b[i]) + carry)%(MAX_BASE_10+1);
      carry = b[i]/(MAX_BASE_10+1);      
    }
    if(carry){
      add_one(b);
    }
    else{
      b[0] ^= 1ULL;
      for(size_t i=1; i;<N;i++)
    b[i] = MAX_BASE_10 - b[i];
    }
  }
  return;
};

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

ОТКУДА ИДТИ

С этого момента нужно сделать много вещей для оптимизации кода, я упомяну несколько, которые я мог придумать:

- Попробуйте заменить добавление массивов возможными вызовами BLAS.

-Убедитесь, что вы используете преимущества векторизации. В зависимости от того, как вы пишете свои циклы, вы можете генерировать или не генерировать векторизованный код. Если ваши массивы станут большими, вы можете извлечь из этого выгоду.

- В духе вышеизложенного убедитесь, что вы правильно выровняли массивы в памяти, чтобы действительно воспользоваться преимуществами векторизации. Насколько я понимаю, std::vector доза не гарантирует выравнивания. Ни дозировать вслепую malloc. Я думаю, что библиотеки boost имеют векторную версию, в которой вы можете объявить фиксированное выравнивание, и в этом случае вы можете запросить 64-битный выровненный массив для вашего массива unsigned long long. Другой вариант — иметь свой собственный класс, который управляет необработанным указателем и выравниванием по дозе с помощью специального распределителя. Заимствование aligned_malloc и aligned_free с сайта https://embeddedartistry.com/blog/2017/02/22/generating-aligned-memory/ у вас может быть такой класс для замены std::vector:

// aligned_malloc and aligned_free from:
// https://embeddedartistry.com/blog/2017/02/22/generating-aligned-memory/

// wrapping in absolutly minimal class to handle
// memory allocation and freeing 
class BigNum{
private:
  unsigned long long *ptr;
  size_t size;
public:  
  BigNum() : ptr(nullptr)
       , size(0)
  {};  

  BigNum(const size_t &N) : ptr(nullptr)
              , size(N)
  {
    resize(N);
  }  
  // Defining destructor this will now delete copy and move constructor and assignment. Make your own if you need them
  ~BigNum(){
    aligned_free(ptr);
  }  

  // Access an object in aligned storage
  const unsigned long long& operator[](std::size_t pos) const{
    return *reinterpret_cast<const unsigned long long*>(&ptr[pos]);
  }
  // return my size
  void size(){    
    return size;
  }
  // resize memory allocation
  void resize(const size_t &N){
    size = N;
    if(N){
      void* temp = aligned_malloc(ptr,N+1); // N+1, always keep first entry for the sign of BigNum 
      if(temp!=nullptr)
    ptr = static_cast<unsigned long long>(temp);
      else
    throw std::bad_alloc();
    }
    else{
      aligned_free(ptr);
    }
  }
};
person Pana    schedule 06.04.2020
comment
unsigned long long имеет ширину 64 бита. как вы пришли к 128 битам? - person DevO; 06.04.2020
comment
Я думал в качестве примера представить 100 бит, и если мой массив состоит из unsigned long long (64 бита), мне нужно три числа в массиве, одно для пения и еще два, которые дадут мне объединенные 128 бит. И тогда нужно иметь дело с переносами, конечно, между этими двумя записями. - person Pana; 06.04.2020
comment
Это действительно хороший ответ, но, как видите, я генерирую N случайных значений по основанию 10 и сохраняю их в массив. Если я просто сгенерирую случайный unsigned long long, результат будет совсем другим, верно? (MyRandom() — это псевдослучайная функция, так что при каждом выполнении результаты будут одинаковыми). Или, может быть, вы говорили о том, что после того, как все значения N будут сгенерированы, свяжите их с длинной длинной переменной? Это вообще возможно? - person Jonathan Sánchez; 06.04.2020
comment
Честно говоря, учитывая ограничение по основанию 10, работа с основанием 2 (о чем я думал в своем первоначальном посте) может быть не лучшим выходом. Каждый unsigned long long (lets shorten this to ull) должен представлять число с основанием 10, поэтому вам нужно будет ограничить максимально допустимое ull количество элементов массива, возможно, с помощью % операции (ull%(max_allowed+1)), и теперь битовые операции не будут тривиальной задачей, поскольку вы будете отслеживать базу 10 операций с представлением по основанию 2. Это потребует немного размышлений, чтобы конкретизировать базу 10. - person Pana; 06.04.2020
comment
Я действительно ценю все усилия, предпринятые для этого вопроса. Я применю эти изменения в своем коде, также достижение векторизации является моей основной целью в этой задаче. Спасибо за совет и ваше время! - person Jonathan Sánchez; 06.04.2020