Могут ли пользовательские классы C ++ воспроизводить производительность встроенных типов? - PullRequest
14 голосов
/ 13 июня 2011

Я пытаюсь создать класс C ++, который ведет себя точно так же, как встроенный тип int, с одним исключением: везде, где вызывается оператор * (или оператор * =), вместо него вызывается сложение.

ВВо-первых, производительность моего класса была очень плохой (1/2 от встроенного int типа), но я заметил, что это потому, что я забыл включить конструктор копирования ниже:

struct AlmostInt {                                                                                                                                                                       

  AlmostInt () { }                
  AlmostInt (const AlmostInt  &a) : val(a.val) { }  // forgetting this killed
                                                    // performance

  AlmostInt operator+(const AlmostInt &a) const { AlmostInt result = *this;
                                          result.val += a.val;
                                          return result; }
  AlmostInt operator-(const AlmostInt &a) const { AlmostInt result = *this;
                                          result.val -= a.val;
                                          return result; }
  AlmostInt operator*(const AlmostInt &a) const { AlmostInt result = *this;
                                          result.val  = result.val + a.val;      
                                          return result; }
  AlmostInt &operator+=(const AlmostInt &a) { this->val += a.val;                           
                                              return *this; }
  AlmostInt &operator-=(const AlmostInt &a) { this->val -= a.val;        
                                              return *this; }
  AlmostInt &operator*=(const AlmostInt &a) { this->val = this->val + a.val);     
                                              return *this; }

private:
  int val;
};

К сожалению,моя программа остается на 25% медленнее, чем должна быть.Изучая сборку, созданную для двух разных версий программы (одна использует int, другая - AlmostInt), я вижу, что число идентичных операций + и - одинаковое, поэтому на каком-то уровне все «работает».

Проблема заключается в том, что в коде значительно больше операций загрузки и хранения с использованием класса AlmostInt, а не собственной операции int.

У кого-нибудь есть идеи относительно того, гдеэти накладные расходы могут быть из?Единственное предположение, которое у меня было, заключалось в том, что, возможно, компилятор не понимает, что AlmostInt имеет все те же свойства, что и int (например, ассоциативность, коммутативность), но если бы это было действительно проблемой, я бы ожидал другое числоИнструкции «+» или «-» в коде, и этого не происходит.

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

Есть идеи?Интересно, кто-нибудь может указать мне на компилятор, который делает позволяющим достичь уровня производительности int с пользовательским классом.

ОБНОВЛЕНИЕ:

Здесьэто простая функция, которую вы можете вырезать и вставить, чтобы увидеть, что происходит для вас.В x86-64 Linux (g ++ 4.3, 4.4), AIX6 xlC и нескольких других платформах изменение строки «CHOOSE ONE ...» ниже должно привести к тому же самому генерируемому коду (или по крайней мерекод той же производительности), но на практике код значительно увеличивается.Может кто-нибудь объяснить, что происходит (для какой-то конкретной платформы / компилятора) или как это исправить?

class AlmostInt
{
    int value;

public:

    AlmostInt& operator+=(AlmostInt that)
    {
        value += that.value;
        return *this;
    }

    AlmostInt& operator-=(AlmostInt that)
    {
        value -= that.value;
        return *this;
    }

        AlmostInt& operator*=(AlmostInt that)
    {
        value *= that.value;
        return *this;
    }
};

AlmostInt operator+(AlmostInt lhs, AlmostInt rhs)
{
    lhs += rhs;
    return lhs;
}

AlmostInt operator-(AlmostInt lhs, AlmostInt rhs)
{
    lhs -= rhs;
    return lhs;
}

AlmostInt operator*(AlmostInt lhs, AlmostInt rhs)
{
    lhs *= rhs;
    return lhs;
}

// CHOOSE ONE OF THE FOLLOWING TWO LINES:
//typedef int real;
typedef AlmostInt real;

typedef struct {
  real re;
  real im;
} complex;

#define R(a0,a1,b0,b1,wre,wim) { \
  t1 = a0 - a1;  t2 = b0 - b1; \
  t5 = t1 * wim; t6 = t2 * wim; \
  t3 = a0;  t1 *= wre; \
  t3 += a1; t2 *= wre; \
  t1 -= t6; t4 = b0; \
  t2 += t5; t4 += b1; \
  a0 = t3;  b1 = t2; \
  a1 = t4;  b0 = t1; \
}

#define RZERO(a0,a1,b0,b1) { \
  t1 = a0 - a1; t2 = b0 - b1; \
  t3 = a0 + a1; t4 = b0 + b1; \
  b0 = t1; a0 = t3; \
  b1 = t2; a1 = t4; \
}

void rpass(real *a, const complex *w, unsigned int n)
{
  real t1, t2, t3, t4, t5, t6, t7, t8;
  real *b;
  unsigned int k;

  b = a + 4 * n;
  k = n - 2;

  RZERO(a[0],a[1],b[0],b[1]);
  R(a[2],a[3],b[2],b[3],w[0].re,w[0].im);
  R(a[4],a[5],b[4],b[5],w[1].re,w[1].im);
  R(a[6],a[7],b[6],b[7],w[2].re,w[2].im);

  for (;;) {
    R(a[8],a[9],b[8],b[9],w[3].re,w[3].im);
    R(a[10],a[11],b[10],b[11],w[4].re,w[4].im);
    R(a[12],a[13],b[12],b[13],w[5].re,w[5].im);
    R(a[14],a[15],b[14],b[15],w[6].re,w[6].im);
    if (!(k -= 2)) break;
    a += 8;
    b += 8;
    w += 4;
  }
}

(Кредит, где кредит должен: этот небольшой тест взят из библиотеки 'djbfft' Дэна Бернштейна)

Ответы [ 4 ]

3 голосов
/ 13 июня 2011

Одна из наиболее частых причин потери производительности в подобных случаях возвращает значения из функций. Теоретически, компилятор должен быть в состоянии оптимизировать это, и сделать то же самое, как если бы вы вернули int (при условии, что все соответствующие функции указаны); на практике все компиляторы, которых я знаю, вернут int в регистре, но для класса типа, передаст дополнительный скрытый аргумент с адресом временно, и вернуть значение в памяти по этому адресу. Причина является то, что такие вещи, как конструктор копирования или присваивание требует адрес (указатель this, ссылка на то, что копируется), и компилятор, кажется, не признает, что, как только он встроен все функции, адрес больше не будет нужен. (Там также Дело в том, что двоичный API говорит сделать это таким образом, но двоичный API обычно касается только структур, а не типов с нетривиальными конструкторы, деструкторы и операторы присваивания.)

1 голос
/ 13 июня 2011

Я бы избавился от конструкторов, заменил бы вызов ссылкой на const на вызов по значению (поскольку объекты AlmostInt действительно малы) и реализовал немодифицирующие операторы как свободные функции:

class AlmostInt
{
    int value;

public:

    AlmostInt& operator+=(AlmostInt that)
    {
        value += that.value;
        return *this;
    }

    AlmostInt& operator-=(AlmostInt that)
    {
        value -= that.value;
        return *this;
    }

    AlmostInt& operator*=(AlmostInt that)
    {
        value *= that.value;
        return *this;
    }
};

AlmostInt operator+(AlmostInt lhs, AlmostInt rhs)
{
    lhs += rhs;
    return lhs;
}

AlmostInt operator-(AlmostInt lhs, AlmostInt rhs)
{
    lhs -= rhs;
    return lhs;
}

AlmostInt operator*(AlmostInt lhs, AlmostInt rhs)
{
    lhs *= rhs;
    return lhs;
}

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

0 голосов
/ 13 июня 2011

Основано на комментариях Fumiyo:

Проверка, чтобы проверить, работает ли NearInt так же быстро, как обычный int на обычной машине:

Вот код, который я запустил: Это умножение матриц(или пытается быть, я не уверен, что понял правильно).Но в любом случае это делает много умножения и сложения, что мы и хотим проверить.Он просто компилируется с -O3

. В функции main () я запускаю умножение 10000 раз.

Результат:

AlmostInt:    10093876
int           10320511

В моих книгах это совпадает снаша допустимая погрешность.

#include <vector>
#include <iostream>
#include <time.h>

#ifdef ALMOST_INT
class AlmostInt
{
    int value;

    public:
    explicit AlmostInt(int i): value(i)  {}
             AlmostInt():      value(0)  {}

    AlmostInt& operator+=(AlmostInt const& that)
    {
        value += that.value;
        return *this;
    }

    AlmostInt& operator-=(AlmostInt const& that)
    {
        value -= that.value;
        return *this;
    }

    AlmostInt& operator*=(AlmostInt const& that)
    {
        value *= that.value;
        return *this;
    }

};

AlmostInt operator+(AlmostInt lhs, AlmostInt const& rhs) { return lhs += rhs; }
AlmostInt operator-(AlmostInt lhs, AlmostInt const& rhs) { return lhs -= rhs; }
AlmostInt operator*(AlmostInt lhs, AlmostInt const &rhs) { return lhs *= rhs; }

#else
typedef int AlmostInt;

#endif


typedef std::vector<AlmostInt>      Vector;
typedef std::vector<Vector>         Matrix;

void mult(Matrix const& a, Matrix const& b, Matrix& r)
{

    for(int x =0; x < 100; ++x)
    {
        for(int y =0; y < 100; ++y)
        {
            AlmostInt   sum(0);
            for(int s = 0;s < 100; ++s)
            {
                sum    += a[s][y] * b[x][s];
            }
            r[x][y] = sum;
        }
    }
}

int main()
{
    Matrix      a(100, Vector(100));
    Matrix      b(100, Vector(100));
    Matrix      r(100, Vector(100));

    clock_t  t   = 0;
    for(int loop=0;loop < 10000; ++loop)
    {
        if ((loop % 100) == 0) std::cout << "Time: " << t << "\n";
        clock_t  s   = clock();
        mult(a, b, r);
        clock_t  e   = clock();
        t += (e-s);
    }
}
0 голосов
/ 13 июня 2011

Возможно, вместо вашего собственного класса вы могли бы делать вещи с коллекцией #define s так:

// For normal operations
#define specialplus +
#define specialmultiple *

// And a separate compilation with
#define specialplus min
#define specialmultiple +

Может быть лучше, как:

// normalmath.c
#define plus(a,b) (a)+(b)
#define star(a,b) (a)*(b)
#define FUNCTYPE normal
#include "yourcode.c"

// tropicalmath.c
#define plus(a,b) min((a),(b))
#define star(a,b) (a)+(b) 
#define FUNCTYPE tropical
#include "yourcode.c"

// yourcode.c
int FUNCTYPE_opp(int x, int y)
{
    // for example
   return star(plus(x,y),52);
}

(по крайней мере, если я не испортил свой код препроцессора C).

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

...