LINUX.ORG.RU

Правильно ли я пишу на Rust?

 , ,


1

7

Часто мелькает Rust и в новостях, и в темах. Решил попробовать переписать один тест с С на Rust. Для сравнения написал вариант и на C++. На Rust получилось в 4+ раза медленнее, чем на С и в 2+ раза медленнее, чем на C++. Есть подозрение, что я делаю что-то неправильно, но не знаю что. Помогите, пожалуйста, разобраться.

UPD. Мои цифры:

$ gcc c_v1.c -Ofast -march=native
$ ./a.out 3000
16.439091
-287.250083
$ g++ cpp_v2.cpp -Ofast -march=native
$ ./a.out 3000
31.3826
-287.25
$ rustc rust_v1.rs -C opt-level=3 -C target-cpu=native
$ ./rust_v1 3000
71.570172703s
-287.2500833333321
★★★

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

Код забыл:

{-# LANGUAGE LambdaCase #-}

module Main (main) where

import System.Clock (Clock(ProcessCPUTime), getTime, toNanoSecs)
import System.Environment (getArgs)
import System.Exit (exitFailure)
import Text.Printf (printf)

type Matrix = [[Double]]

main :: IO ()
main = do
  n <- getArgs >>= \case
    [a] -> pure (read a)
    _ -> exitFailure

  let a = newMatrix n
      b = newMatrix n
      c = newMatrix n

  t1 <- clock
  let c' = matrixMult n a b c
  t2 <- clock

  printf "%dns\n" (t2 - t1)
  printf "% 8.6f\n" (c' !! (n `div` 2) !! (n `div` 2))

newMatrix :: Int -> Matrix
newMatrix n =
  let tmp = 1 / fromIntegral n / fromIntegral n in
  [ [ tmp * (i - j) * (i + j)
    | j <- [0 .. fromIntegral (pred n)]
    ]
  | i <- [0 .. fromIntegral (pred n)]
  ]

matrixMult :: Int -> Matrix -> Matrix -> Matrix -> Matrix
matrixMult n a b _ =
  let t = [ [ b!!j!!i
            | j <- [0 .. pred n]
            ]
          | i <- [0 .. pred n]
          ]
      c = [ [ sum [ a!!i!!k * t!!j!!k | k <- [0 .. pred n] ]
            | j <- [0 .. pred n]
            ]
          | i <- [0 .. pred n]
          ]
  in c

matrixPrint :: Int -> Matrix -> String
matrixPrint _ = unlines . fmap (unwords . fmap (printf "% 8.6f"))

clock :: IO Integer
clock = toNanoSecs <$> getTime ProcessCPUTime
Laz ★★★★★
()
Ответ на: комментарий от Laz

Если в конец main добавить

  t3 <- clock
  printf "%dns\n" (t3 - t2)

То получим:

$ ghc -O test9.hs
[1 of 1] Compiling Main             ( test9.hs, test9.o )
Linking test9 ...
$ ./test9 3000
0ns
-287.250083
921875000ns
red75prim ★★★
()
Ответ на: комментарий от red75prim

Lazy evaluation. Вычисляется только c[n/2, n/2]. Так?

Всё так. Но вообще, бенчмарк не имеет смысла, так как подобный код на любом (нормальном) языке будет компилироваться примерно в одно и то же.

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

Тут скорее сравнивается насколько языки «zero-cost», то есть вносят ли они дополнительные накладные расходы. Для C, C++ и Rust получается примерно одинаково.

При прочих равных, конечно. Анонимус выше вон всех обогнал за счёт LLVM 10.

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

хотя haskell на такой численной задаче явно в проигрыше

С таким тупым кодом - да, и дело не в числах, а в списках. Но, полагаю, если взять мутабельные вектора, результат будет не сильно отличаться от си.

Laz ★★★★★
()
Ответ на: комментарий от red75prim
#include <vector>
#include <ctime>
#include <valarray>
#include <cstdlib>
#include <cstdio>
#include <numeric>

using matrix_t = std::vector<std::valarray<double>>;
auto matrix = [](size_t n) { return matrix_t{n, std::valarray<double>(n)}; };


void fill(matrix_t & m) {
  auto n = m.size();
  auto tmp = 1. / n / n;
  for(ssize_t i = 0; i < ssize_t(n); ++i)
    for(ssize_t j = 0; j < ssize_t(n); ++j)
      m[i][j] = tmp * (i - j) * (i + j);
}

[[gnu::always_inline]] inline void mult(const matrix_t & a, const matrix_t & b, matrix_t & c) {
  auto n = a.size();
  auto tmp = matrix(n);
  for(size_t i = 0; i < n; ++i) {
    for(size_t j = 0; j < n; ++j)
      tmp[i][j] = b[j][i];
  }
  
  for(size_t i = 0; i < n; ++i) {
    for(size_t j = 0; j < n; ++j) {
//       c[i][j] = [](auto & a, auto & b) {
//         double acc = 0;
//         for(size_t n = 0; n < a.size(); ++n) acc += a[n] * b[n];
//         return acc;
//       }(a[i], tmp[j]);
      c[i][j] = std::inner_product(begin(a[i]), end(a[i]), begin(tmp[j]), 0.);
    }
  }
}


int main(int argc, char * argv[]) {
  size_t N = 3000;
  if(argc > 1) N = std::atol(argv[1]);

  fprintf(stderr, "%lu\n", N);

  auto a = matrix(N), b = matrix(N), c = matrix(N);

  fill(a);
  fill(b);
  
  clock_t t1 = clock();
  mult(a, b, c);
  clock_t t2 = clock();
  fprintf(stderr, "%f\n", (double)(t2 - t1) / CLOCKS_PER_SEC);
  fprintf(stderr, "%f\n", c[N/2][N/2]);
} 

На, я даже тебе fb на бич-версия написал, пробуй это.

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

Если взять мутабельные и unboxed, то да, результат на haskell будет близок к системным языкам, но далеко не каждый умеет писать такой код. Что хорошего в Си или Фортране - там очевидно написанный код для численных методов и работает быстро. Если взять ту же растишку, то уже нужна была помощь виртуозов. А вот с haskell еще сложнее. Там нужно понимать, где использовать unboxed, где seq и так далее.

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

Если взять ту же растишку, то уже нужна была помощь виртуозов.

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

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

Да, время в наносекундах.

Жаль, правда, за это время программа на хаскеле ничего не сделала. Реальный результат можно получить так:

c'' <- let c' = matrixMult n a b c in c' `deepseq` pure c'

Далее:

type Matrix = [[Double]]

Хаскельные ленивые списки совершенно не подходят для задачи перемножения матриц. Более того, они вообще не являются хорошим способом персистентного хранения данных. Хаскельные ленивые списки это скорее аналог итераторов из Rust'а. В данном случае нужно было использовать Vector, и создавать новую перемноженную матрицу в ST монаде.

В целом вообще глупо в данном сравнении приводить Haskell. Rust, C и C++, языки с минимальным рантаймом, они созданны для подобных числодробилок. Haskell же высокоуровневый язык с развесистым рантаймом, в котором есть и GC и легковесные потоки и средства для удобного распараллеливания чистых вычислений. Даже если эту задачу решить на Haskell правильно, всё равно он не догонит решения на Rust/C/C++.

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

программа на хаскеле ничего не сделала

Не знаю, сделала она что-то или нет, подходят списки или нет, но, при тупой трансляции сишной реализации алгоритма из ОП в хаскель, выдаваемый ответ в точности тот же, время работы на порядок меньше, а посчитанные попугаи (снятые, судя по коду, в тех же местах) меньше на несколько порядков. Как на это реагировать - это уже другой вопрос. Почему-то классический пример take 5 . sort, который не сортирует список полностью, вызывает восторг, а мой пример, который не перемножает матрицы до конца, вызывает бомбёжку.

Даже если эту задачу решить на Haskell правильно, всё равно он не догонит решения на Rust/C/C++.

Интересно, на сколько сильно не догонит. Мне лень писать код, но подозреваю, что мутабельные анбоксед вектора будут выглядеть довольно достойно.

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

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

Там измеряется время создания thunk’а. Собственно вычисление происходит при выводе значения на печать.

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

Твой код не делает то, что замеряет тс. Можно так же сделать все вычисления ленивыми на С++ (модераторы потёрли) или Rust и запросить только выводимый элемент. Только к обсуждаемой теме это не относится.

  time = 62ns
     n = 3000
result = -287.25008333333216
use std::{env, time::Instant};

fn main() {
    let args: Vec<_> = env::args().collect();
    let n = args[1].parse().unwrap();

    let a = &matrix_new(n);
    let b = &matrix_new(n);

    let t1 = Instant::now();
    let mut c = (0..n).map(move |_| {
        (0..n).map(move |i| (0..n).map(|j| a[i * n + j] * b[j * n + i]).sum::<f64>())
    });

    let t2 = Instant::now();

    println!("  time = {:?}", t2.duration_since(t1));
    println!("     n = {}", n);
    println!("result = {}", c.nth(n / 2).unwrap().nth(n / 2).unwrap());
}

fn matrix_new(n: usize) -> Vec<f64> {
    let tmp = 1.0 / n as f64 / n as f64;
    (0..n)
        .flat_map(|i| (0..n).map(move |j| tmp * (i as isize - j as isize) as f64 * (i + j) as f64))
        .collect()
}

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

Ты уже запарил.

use std::{env, time::Instant};

fn main() {
    let args: Vec<_> = env::args().collect();
    let n = args[1].parse().unwrap();

    let tmp = 1.0 / n as f64 / n as f64;
    let a = (0..n)
        .map(|i| (0..n).map(move |j| tmp * (i as isize - j as isize) as f64 * (i + j) as f64));
    let b = (0..n)
        .map(|i| (0..n).map(move |j| tmp * (j as isize - i as isize) as f64 * (i + j) as f64));

    let t1 = Instant::now();
    let mut c = a.map(move |row| {
        b.clone().map(move |col| {
            let row = row.clone();
            move || col.zip(row).map(|(b, a)| a * b).sum::<f64>()
        })
    });
    let t2 = Instant::now();

    println!("  time = {:?}", t2.duration_since(t1));
    println!("     n = {}", n);
    println!("result = {}", c.nth(n / 2).unwrap().nth(n / 2).unwrap()());
}

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