LINUX.ORG.RU

Сравнение быстродействия виртуальных методов С++ и CLOS

 ,


0

2

Было бы интересно сравнить производительность вирт. функций.

Нужно учесть следующее: доступ к полям в виде родовых функций намного-намного медленнее доступа к полям структуры, за исключением частных случаев (например, в теле метода, специфицированного этим классом, может быть лишь раза в полтора медленнее структуры, согласно рук-ву SBCL). Кроме того, в С++ есть статические методы, к-рых в CLOS вроде бы как нет. Далее, я не смог заставить CLOS заинлайнить вызовы вирт.ф-й, а в С++ это в принципе возможно (хотя я не понял, заинлайнены ли они в нашем случае).

По этой причине я взял вместо классов структуры (и в жизни тоже всегда использую структуры вместо классов).

Код такой, но за качество не ручаюсь. Добровольцы на code review приглашаются.

;; clos-virtual-method-benchmark.lisp
(in-package :cl-user)

(declaim
 (optimize (speed 3) (safety 0) (debug 0)
           (space 0) (compilation-speed 0)))

(proclaim
 '(optimize (speed 3) (safety 0) (debug 0)
           (space 0) (compilation-speed 0)))


(defparameter *times* (* 100 1000 1000))

(defstruct base 
  next
  )

(defstruct (test-class-3 (:include base))
  fld1
  fld2
  )

(declaim (sb-ext:maybe-inline Делишко+))
(proclaim '(sb-ext:maybe-inline Делишко+))

(declaim (inline Делишко+))
(proclaim '(inline Делишко+))

(defgeneric Делишко+ (ц))

(defmethod Делишко+ ((ц base))
  7)

(defmethod Делишко+ ((ц test-class-3))
  (setf (test-class-3-fld1 ц) (test-class-3-fld2 ц))
  1)

(defmethod Делишко+ ((ц string))
  0)

(defun inner (o n)
  (declare (type fixnum n))
  (declare (type base o))
  (declare (inline Делишко+))
  (let ((res 0))
    (declare (type fixnum res))
    (dotimes (i n)
      (incf res (the fixnum (Делишко+ o)))
      (incf res (the fixnum (Делишко+ o)))
      (incf res (the fixnum (Делишко+ o)))
      (incf res (the fixnum (Делишко+ o)))
      (incf res (the fixnum (Делишко+ o)))
      (incf res (the fixnum (Делишко+ o)))
      (incf res (the fixnum (Делишко+ o)))
      (incf res (the fixnum (Делишко+ o)))
      (incf res (the fixnum (Делишко+ o)))
      (incf res (the fixnum (Делишко+ o)))
      (setf res (mod res 16))
      (setf o (base-next o)))
    (print res)
    res))

(defun main()
  (let* (
         (o1 (make-test-class-3 :fld1 1))
         (o2 (make-test-class-3 :fld1 1 :next o1))
         res)
    (setf (test-class-3-next o1) o2)
    (setf res (inner o1 *times*))
    (format t "~%~S~%" res)))

(let ((*trace-output* *standard-output*))
  (time (main)))

;; eof
6.7 секунды

С++

// cpp-virtual-method-benchmark.cpp
#include "stdio.h"
#include <cstdlib>

class base_class {
public:
        base_class* next;
        virtual int meth() { return 0; };
};

class test_class : public base_class {
public:
	int fld1, fld2, fld3, fld4, fld5;
        int meth () { fld4 = fld2; return 1; };
};


int inner(base_class *o,int bound) {
    int res=0;
    for (int i=0;i<bound;i++) {
        res += o->meth();
        res += o->meth();
        res += o->meth();
        res += o->meth();
        res += o->meth();
        res += o->meth();
        res += o->meth();
        res += o->meth();
        res += o->meth();
        res += o->meth();
        o = o->next;
        res = res % 16;

    }    
    return res;
}

int main(int argc, char* argv[])
{
    test_class o1;
    test_class o2;
    o1.fld1=1;
    o2.fld1=1;
    o1.next=&o2;
    o2.next=&o1;
    int n = 100*1000*1000;
    int result=inner(&o1,n);
    printf("%d %d\n",o1.fld5,result); // проверяем корректность и чтобы оптимизатор
    // не выкинул неиспользуемый код
    return 0;
}

// g++ -O2 -o cpp-benchmark cpp-virtual-method-benchmark.cpp ; echo disassemble inner | gdb cpp-benchmark ; time ./cpp-benchmark 
1.54 секунды.

Ну и для полноты картины, создаваемая объектная система для Яра:

  • Полностью оптимизирована под отладку, смещение в VMT выражено глобальной переменной, вызов функции через символ - 31 секунда.
  • то же, но вызов функции как функции 16 секунд
  • то же, оптимизация под скорость - 4.2 секунды

Получилось в 1.6 раза быстрее CLOS и в 2.7 раз медленнее С++. Правда, для множественной диспетчеризации скорее всего проиграем против CLOS. С другой стороны, сделать inline статический диспатч для CLOS методов в случае, когда тип достаточно хорошо известен, мне не удалось, оно делается с помощью MOP. Я делаю примерно то же, но без CLOS.

★★★★★

Последнее исправление: den73 (всего исправлений: 8)

в С++ есть статические методы плюс к тому inline.

Как это боком к виртуальным методам?

Компилятор может вызвать нужный метод напрямую, без vtable, если сообразит. Но это не inline.

RazrFalcon ★★★★★
()
Ответ на: комментарий от RazrFalcon

Компилятор может вызвать нужный метод напрямую, без vtable, если сообразит. Но это не inline.

Там есть

inline
void Fred::f(int i, char c)
{
  // ...
}

monk ★★★★★
()
Ответ на: комментарий от RazrFalcon

«The only time an inline virtual call can be inlined is when the compiler knows the „exact class“ of the object which is the target of the virtual function call.»

Имеет. TempDerived aDerivedObj; TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction(); инлайнится.

monk ★★★★★
()

Сложность вызова обобщенной (англ. generic) функции зависит от того, сколько параметров у функции и какие они, а также от того, насколько развесистая иерархия классов, реализующая в методах данную функцию.

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

Обобщенные функции не могут быть быстрыми по определению, хотя питончик, может быть, и обгонят, но то питончик

dave ★★★★★
()
Последнее исправление: dave (всего исправлений: 2)

Ты опять делаешь какие-то странные сравнения. Я не знаю за CL, но в плюсах вызов виртуального метода довольно очевидно компилируется в indirect call и ничего больше. см. https://godbolt.org/g/MVFp21 .

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

nwalker
()
Ответ на: комментарий от nwalker

«Ничего больше» имеет свою цену, вот я её и выясняю.

Лучше разберись, как смотреть сгенерированный CL код.

Ты думаешь, я не разобрался? (disassemble 'имя-функции) . Что теперь? ВОпрос-то по сути не в том, как разобраться, а как заставить его работать быстрее.

den73 ★★★★★
() автор топика
Ответ на: комментарий от dzidzitop

Ну человек явно пообщаться пришел. Надо как-то завести беседу...

four_str_sam
()
Ответ на: комментарий от nwalker

Ты опять делаешь какие-то странные сравнения. Я не знаю за CL...

Не рассматривайся, Дениска знает CL хуже тебя

anonymous
()
(declaim
 (optimize (speed 3) (safety 0) (debug 0)
           (space 0) (compilation-speed 0)))

(proclaim
 '(optimize (speed 3) (safety 0) (debug 0)
           (space 0) (compilation-speed 0)))

Для уверенности. Убейся, а?

anonymous
()

Кстати, почему тебя так долго (уже пару дней) небыло на лиспер.сру? Пойдем туда, я обложу тебя хуями

anonymous
()
Ответ на: комментарий от dave

Обобщенные функции не могут быть быстрыми по определению

Ты здесь называешь обобщённым функциями конкретно виртуальные вызовы конкретно в плюсах? Потому что тех же плюсах обычно обобщёнными функциями называют шаблоны, и они таки быстрые. Не говоря уже о других языках

Crocodoom ★★★★★
()
Ответ на: комментарий от Crocodoom

Нет, речь о лиспе. Ты с ним не знаком?

dave ★★★★★
()
Ответ на: комментарий от dzidzitop

Клинически-каноничный тест, который не имеет никакого смысла и вообще ничего не меряет.

Плюсую. Но для того, чтобы объяснить почему это так, нужно написать несколько статей с примерами.

Разговор был бы чуть более конструктивным, если бы ТС написал, для чего он все это затеял.

anonymous
()
Ответ на: комментарий от Crocodoom

Речь идёт о generic functions, к-рые по-русски называют ещё родовыми функциями и они отличаются от виртуальных функций С++ тем, что динамический выбор метода происходит не по одному, а по нескольким аргументам.

den73 ★★★★★
() автор топика
Последнее исправление: den73 (всего исправлений: 1)
Ответ на: комментарий от dave

Сложность вызова обобщенной (англ. generic) функции зависит от того, сколько параметров у функции и какие они, а также от того, насколько развесистая иерархия классов, реализующая в методах данную функцию.

Не исключаю, что от сложности иерархии не зависит. Например, (typep объект структура) вычисляется в SBCL за О(1) независимо от глубины наследования. Понятно, что в целом зависит и от числа аргументов, но у меня нет ресурсов на более полное сравнение. Я думаю, что моё сравнение вполне корректное, хотя и не полное.

хотя питончик, может быть, и обгонят, но то питончик

PHP тоже обгоняет питончик. А вдруг PHP обгонит CLOS? Получится: «с точки зрения производительности CL не предоставляет преимуществ перед PHP, если использовать объекты».

den73 ★★★★★
() автор топика

Было бы интересно сравнить в чем-то чуть __более__ сложном вроде двух одинаково-написанных клиент-серверных приложения, выполняющего определенные задачи. Тогда да – тестирование и исследование, запарился, красава и всё такое, а тут ничьего.

anonymous
()
Ответ на: комментарий от anonymous

Разговор был бы чуть более конструктивным, если бы ТС написал, для чего он все это затеял.

В лисп-сообществах ТСа игнорит. Это самое конструктивное, что смогли придумать.

anonymous
()
Ответ на: комментарий от anonymous

Напротив, я уже игнорю тот же лиспер.сру из-за засилия на нём ТСа, который разговаривает там сам с собой и с другим укурком монком. Да и на лиспе уже давно не писал

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.