Большие целые числа & C - PullRequest
0 голосов
/ 06 апреля 2020

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

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

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

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);
}

1 Ответ

2 голосов
/ 06 апреля 2020

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

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

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

- n_s. Для представления знака + и - только знак 1 и знак

- займет только 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) Поскольку тип не подписан 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. С помощью этой базовой настройки c теперь можно перейти к некоторому актуальному коду. Сейчас я буду использовать 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... Комплимент ди git теперь будет определяться как:

     `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 + carry (i-1)
  • convert -> 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;
};

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

ГДЕ GO ОТ ЗДЕСЬ

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

-Попробуйте и замените добавление массивов возможными вызовами 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);
    }
  }
};
...