Можно ли ускорить этот микробенчмарк Chez Scheme?

Этот двойной цикл в Chez Scheme в 50 раз медленнее, чем в C++ (скомпилирован с --optimize-level 3 и -O3 соответственно).

(import
  (rnrs)
  (rnrs r5rs))


(let* ((n (* 1024 16))
       (a (make-vector n))
       (acc 0))

  (do ((i 0 (+ i 1)))
    ((= i n) #f)
    (vector-set! a i (cons (cos i) (sin i))))

  (do ((i 0 (+ i 1)))
    ((= i n) #f)
    (do ((j 0 (+ j 1)))
      ((= j n) #f)
      (let ((ai (vector-ref a i))
            (aj (vector-ref a j)))
        (set! acc (+ acc (+ (* (car ai) (cdr aj))
                            (* (cdr ai) (car aj))))))))

  (write acc)
  (newline))

(exit)

vs

#include <iostream>
#include <cmath>
#include <vector>
#include <algorithm>

typedef std::pair<double, double> pr;

typedef std::vector<pr> vec;

double loop(const vec& a)
{
    double acc = 0;
    const int n = a.size();

    for(int i = 0; i < n; ++i)
        for(int j = 0; j < n; ++j)
        {
            const pr& ai = a[i];
            const pr& aj = a[j];
            acc += ai .first * aj.second + 
                   ai.second * aj .first;
        }

    return acc;
}

int main()
{
    const int n = 1024 * 16;

    vec v(n);

    for(int i = 0; i < n; ++i)
        v[i] = pr(std::cos(i), std::sin(i));

    std::cout << loop(v) << std::endl;
}

Я понимаю, что в Scheme больше косвенного обращения к памяти, чем в C++, но все же разница в производительности удивительна...

Есть ли простой способ ускорить версию Scheme? (Без изменения макета памяти на что-то совершенно однообразное)


person bobcat    schedule 27.09.2018    source источник


Ответы (1)


Таким образом, хотя эти программы выглядят одинаково, они не одинаковы. Вы используете арифметику fixnum в версии C, в то время как версия Scheme использует стандартную числовую башню. Чтобы сделать версию C более похожей на Scheme, попробуйте использовать библиотеку bignum для своих вычислений.

В качестве теста я заменил арифметику на (rnrs arithmetic flonums) и (rnrs arithmetic fixnums), и это вдвое сократило время выполнения в DrRacket. Я ожидаю, что то же самое произойдет в любой реализации.

Теперь мои первоначальные тесты показали, что код C выполняется примерно в 25 раз быстрее, а не в 50 раз, как ожидалось, и, перейдя на арифметику с плавающей запятой, я стал примерно в 15 раз быстрее.

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

Надеюсь, это поможет и в Chez :)

ИЗМЕНИТЬ

Вот мой модифицированный исходник, который увеличивает скорость в 2 раза:

#!r6rs
(import
 (rnrs)
 ;; import the * and + that only work on floats (which are faster, but they still check their arguments)
 (only (rnrs arithmetic flonums) fl+ fl*))


(let* ((n (* 1024 16))
       (a (make-vector n))
       (acc 0.0)) ; We want float, lets tell Scheme about that!

  ;; using inexact f instead of integer i
  ;; makes every result of cos and sin inexact
  (do ((i 0 (+ i 1))
       (f 0.0 (+ f 1)))
    ((= i n) #f)
    (vector-set! a i (cons (cos f) (sin f))))

  (do ((i 0 (+ i 1)))
    ((= i n) #f)
    (do ((j 0 (+ j 1)))
      ((= j n) #f)
      (let ((ai (vector-ref a i))
            (aj (vector-ref a j)))
        ;; use float versions of + and *
        ;; since this is where most of the time is used
        (set! acc (fl+ acc
                       (fl+ (fl* (car ai) (cdr aj))
                            (fl* (cdr ai) (car aj))))))))

  (write acc)
  (newline))

И конкретная реализация (блокировка) просто для того, чтобы сказать, что проверка типов, выполненная во время выполнения, действительно имеет влияние, этот код работает на 30% быстрее, чем предыдущая оптимизация:

#lang racket

;; this imports import the * and + for floats as unsafe-fl* etc. 
(require racket/unsafe/ops)

(let* ((n (* 1024 16))
       (a (make-vector n))
       (acc 0.0)) ; We want float, lets tell Scheme about that!

  (do ((i 0 (+ i 1))
       (f 0.0 (+ f 1)))
    ((= i n) #f)
    ;; using inexact f instead of integer i
    ;; makes every result of cos and sin inexact
    (vector-set! a i (cons (cos f) (sin f))))

  (do ((i 0 (+ i 1)))
    ((= i n) #f)
    (do ((j 0 (+ j 1)))
      ((= j n) #f)
      ;; We guarantee argument is a vector
      ;; and nothing wrong will happen using unsafe accessors
      (let ((ai (unsafe-vector-ref a i))
            (aj (unsafe-vector-ref a j)))
        ;; use unsafe float versions of + and *
        ;; since this is where most of the time is used
        ;; also use unsafe car/cdr as we guarantee the argument is
        ;; a pair.
        (set! acc (unsafe-fl+ acc
                              (unsafe-fl+ (unsafe-fl* (unsafe-car ai) (unsafe-cdr aj))
                                          (unsafe-fl* (unsafe-cdr ai) (unsafe-car aj))))))))

  (write acc)
  (newline))

Я постарался сохранить стиль исходного кода. Это не очень идиоматическая схема. Например. Я бы вообще не стал использовать set!, но на скорость это не влияет.

person Sylwester    schedule 27.09.2018
comment
Вы используете -O3 с g++? - person bobcat; 28.09.2018
comment
@MaxB Да. C запустился за 0.25s и скомпилировал Scheme за 2.5s, в то время как исходный код запустился примерно за 6.5s (я взял в среднем 20 вызовов с использованием time) - person Sylwester; 28.09.2018
comment
Не могли бы вы вставить код своей схемы в ответ? Я предполагаю, что вы изменили все вызовы арифметических функций на их неуниверсальные версии? - person bobcat; 28.09.2018
comment
@MaxB Конечно. Последнее не является схемой, но и не дает наибольшего улучшения скорости. - person Sylwester; 28.09.2018
comment
Самая прямая аналогия C++ std::vector - это не вектор схемы (который является гетерогенным), а схема байтевектора. Наиболее точное сравнение по скорости будет сравнивать std::vector с целочисленными операциями R6RS с байт-вектором фиксированного размера. Это почти наверняка будет быстрее, поскольку позволяет избежать косвенного указателя, присутствующего в векторе схемы. - person Chris Vine; 29.09.2018