Go быстрее Rust, Mail.Ru Group сделала замеры / Хабрахабр

С такой фразой мне кинули ссылку на статью компании Mail.Ru Group от 2015 «Как выбрать язык программирования?». Если кратко, они сравнили производительность Go, Rust, Scala и Node.js. За первое место боролись Go и Rust, но Go победил.

Как написал автор статьи gobwas (здесь и далее орфография сохранена):

Эти тесты показывают, как ведут себя голые серверы, без «прочих нюансов» которые зависят от рук программистов.

К моему большому сожалению, тесты не были эквивалентными, ошибка всего лишь в 1 строчке кода поставила под сомнение объективность и вывод статьи.

В статье будет много копипасты из исходной статьи, но я надеюсь, что мне это простят.

Суть тестов

При тестировании выяснилось, что все претенденты работают примерно с одинаковой производительностью в такой постановке — все упиралось в производительность V8. Однако реализация задания не была лишней — разработка на каждом из языков позволила составить значительную часть субъективных оценок, которые так или иначе могли бы повлиять на окончательный выбор.

Итак, мы имеем два сценария. Первый — это просто приветствие по корневому URL:

GET / HTTP/1.1
Host: service.host

HTTP/1.1 200 OK

Hello World!

Второй — приветствие клиента по его имени, переданному в пути URL:

GET /greeting/user HTTP/1.1
Host: service.host

HTTP/1.1 200 OK

Hello, user

Первоначальный исходный код тестов

Подлый удар в спину

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

Don’t click

Дело в том, что в примере Node.js и Go компиляция регулярного выражения происходит единожды, тогда как в Rust компиляция выполняется на каждый запрос. Про Scala ничего сказать не могу.

Выдержка из документации к regex для Rust:

Example: Avoid compiling the same regex in a loop

It is an anti-pattern to compile the same regular expression in a loop since compilation is typically expensive. (It takes anywhere from a few microseconds to a few milliseconds depending on the size of the regex.) Not only is compilation itself expensive, but this also prevents optimizations that reuse allocations internally to the matching engines.

In Rust, it can sometimes be a pain to pass regular expressions around if they’re used from inside a helper function. Instead, we recommend using the lazy_static crate to ensure that regular expressions are compiled exactly once.

For example:

#[macro_use] extern crate lazy_static;
extern crate regex;

use regex::Regex;

fn some_helper_function(text: &str) -> bool {
    lazy_static! {
        static ref RE: Regex = Regex::new("...").unwrap();
    }
    RE.is_match(text)
}

fn main() {}


Specifically, in this example, the regex will be compiled when it is used for the first time. On subsequent uses, it will reuse the previous compilation.

Выдержка из документации к regex для Go:

But you should avoid the repeated compilation of a regular expression in a loop for performance reasons.

Как допустили такую ошибку? Я не знаю… Для такого прямолинейного теста это является существенной просадкой в производительности, ведь даже в комментариях автор указал на тормознутость регулярок:

Спасибо! Я тоже думал было переписать на split во всех примерах, но потом показалось, что с regexp будет более жизненно. При оказии попробую прогнать wrk со split.

Упс.

Восстанавливаем справедливость

Исправленный тест Rust

extern crate hyper;
extern crate regex;

#[macro_use] extern crate lazy_static;

use std::io::Write;
use regex::{Regex, Captures};

use hyper::Server;
use hyper::server::{Request, Response};
use hyper::net::Fresh;
use hyper::uri::RequestUri::{AbsolutePath};

fn handler(req: Request, res: Response<Fresh>) {
    lazy_static! {
        static ref GREETING_RE: Regex = Regex::new(r"^/greeting/([a-z]+)$").unwrap();
    }

    match req.uri {
        AbsolutePath(ref path) => match (&req.method, &path[..]) {
            (&hyper::Get, "/") => {
                hello(&req, res);
            },
            _ => {
                greet(&req, res, GREETING_RE.captures(path).unwrap());
            }
        },
        _ => {
            not_found(&req, res);
        }
    };
}

fn hello(_: &Request, res: Response<Fresh>) {
    let mut r = res.start().unwrap();
    r.write_all(b"Hello World!").unwrap();
    r.end().unwrap();
}

fn greet(_: &Request, res: Response<Fresh>, cap: Captures) {
    let mut r = res.start().unwrap();
    r.write_all(format!("Hello, {}", cap.at(1).unwrap()).as_bytes()).unwrap();
    r.end().unwrap();
}

fn not_found(_: &Request, mut res: Response<Fresh>) {
    *res.status_mut() = hyper::NotFound;
    let mut r = res.start().unwrap();
    r.write_all(b"Not Foundn").unwrap();
}

fn main() {
    let _ = Server::http("127.0.0.1:3000").unwrap().handle(handler);
}

Я умышленно исправил только лишь багу, а стиль кода оставил без изменений.

Окружение

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

  1. Ноут
    • Intel® Core(TM) i7-6820HQ CPU @ 2.70GHz, 4+4
    • CPU Cache L1: 128 KB, L2: 1 MB, L3: 8 MB
    • 8+8 GB 2133MHz DDR3

  2. Десктоп
    • Intel® Core(TM) i3 CPU 560 @ 3.33GHz, 2+2
    • CPU Cache L1: 64 KB, L2: 4 MB
    • 4+4 GB 1333MHz DDR3

  3. go 1.6.2, released 2016/04/20
  4. rust 1.5.0, released 2015/12/10. Да, я специально взял старую версию Rust.
  5. Простите, любители Scala и Node.js, этот холивар не про вас.

Интрига

ab

Попробуем выполнить 50 000 запросов за 10 секунд, с 256 возможными параллельными запросами.

Десктоп

ab -n50000 -c256 -t10 "http://127.0.0.1:3000/




LabelTime per request, msRequest, #/sec
Rust11.72921825.65
Go13.99218296.71

ab -n50000 -c256 -t10 "http://127.0.0.1:3000/greeting/hello"




LabelTime per request, msRequest, #/sec
Rust11.98221365.36
Go14.58917547.04

Ноут

ab -n50000 -c256 -t10 "http://127.0.0.1:3000/"




LabelTime per request, msRequest, #/sec
Rust8.98728485.53
Go9.83926020.16

ab -n50000 -c256 -t10 "http://127.0.0.1:3000/greeting/hello"




LabelTime per request, msRequest, #/sec
Rust9.14827984.13
Go9.68926420.82

— Подожди, — скажет читатель. — И стоило тебе строчить статью ради каких-то 500rps?! Ведь это доказывает, что не важно на чем писать, все языки одинаковые!

И тут вступает в дело мой шнур. Шнур для зарядки ноутбука, разумеется.

Ноут на подзарядке

ab -n50000 -c256 -t10 "http://127.0.0.1:3000/"




LabelTime per request, msRequest, #/sec
Rust5.60145708.98
Go6.77037815.62

ab -n50000 -c256 -t10 "http://127.0.0.1:3000/greeting/hello"




LabelTime per request, msRequest, #/sec
Rust5.73644627.28
Go6.45139682.85

Стой, Go, ты куда?

Выводы

Я допускаю, что статья компании Mail.Ru Group содержала непреднамеренную ошибку. Тем не менее, за 1.5 года ее прочитали 45 тысяч раз, и её выводы могли сформировать предвзятое отношение в пользу Go при выборе инструментов, ведь Mail.Ru Group, несомненно, прогрессивная и технологичная компания, к чьим словам стоит прислушаться.

И всё это время Rust совершенствовался, посмотрите на «The Computer Language Benchmarks Game» Rust vs Go за 2015 и 2017 года. Отрыв в производительности только растет.

Если тебе, дорогой читатель, по нраву Go, пиши на нём. Но не стоит сравнивать его производительность с Rust, ибо она будет не в пользу твоего любимого языка, уж точно не на таких синтетических тестах.

А если тебе нравится Rust, вливайся в сообщество, нам многого не хватает. Того же Tox.

Я надеюсь, что я был объективен и непредвзят, справедливость восторжествовала, а моя статья не содержит ошибок.

Let the Holy War begin!

Источник