Какую цену мы платим за использование async/await в языках JS / C# / Rust

?v=1

Привет, Хабр !

Работая с Javascript / Typescript, я давно заметил, что асинхронное API работает медленней чем аналогичное синхронное, и даже знал что так должно быть. Но на последнем проекте асинхронная работа с файловой системой стала узким местом, и я озаботился замерами.

Известно, что await можно использовать только внутри функций или блоков async, а это значит, что если у вас самый нижний уровень API асинхронный, то придется использовать async/await практически везде, даже там, где оно очевидно не нужно.

К примеру, мы пишем сервисную функцию, которая достает из хранилища объект по ключу. В качестве хранилища мы можем использовать файл, БД, микросервис, то есть медленный источник с асинхронным интерфейсом. Для улучшения производительности — внутри нашей функции мы кэшируем ранее извлеченные объекты (складываем их в Map). По мере работы программы — реальных обращений к хранилищу становится все меньше, объекты отдаются из быстрого кэша, но интерфейс функции остается асинхронным!

Какую цену мне приходится платить за каждый асинхронный вызов?
Результаты тестов удручают…

Возьмем простую функцию, пометим ее async, и будем вызывать в цикле, замеряя общее время, и сравнивая с аналогичным синхронным кодом. Для сравнения синтаксиса привожу полные тексты на 3-х языках.


Typescript (Deno)


Синхронный код
const b = Date.now()
let j = 0.0
for (let i = 0; i < 1_000_000_000; i++) {
    j += f(i)
}
console.log(j + ', ' + (Date.now() - b)/1000 + 's')

function f(i: number): number {
    return i / 3.1415926
}

Асинхронный код:

(async () => {
    const b = Date.now()
    let j = 0.0
    for (let i = 0; i < 1_000_000_000; i++) {
        j += await f(i)
    }
    console.log(j + ', ' + (Date.now() - b)/1000 + 's')
})()

async function f(i: number): Promise {
    return i / 3.1415926
}


C# (.NET Core)


Синхронный код
using System;

class App {
    static void Main(string[] args) {
        var b = DateTime.Now;
        var j = 0.0;
        for (var i = 0L; i < 1_000_000_000L; i++) {
            j += f(i);
        }
        Console.WriteLine(j + ", " + (DateTime.Now - b).TotalMilliseconds / 1000 + "s");
    }

    static double f(long i) {
        return i / 3.1415926;
    }
}

Асинхронный код:

using System;
using System.Threading.Tasks;

class App {
    static async Task Main(string[] args) {
        var b = DateTime.Now;
        var j = 0.0;
        for (var i = 0L; i < 1_000_000_000L; i++) {
            j += await f(i);
        }
        Console.WriteLine(j + ", " + (DateTime.Now - b).TotalMilliseconds / 1000 + "s");
    }

    static async Task f(long i) {
        return i / 3.1415926;
    }
}


Rust


Синхронный код
fn main() {
    let tbegin = std::time::SystemTime::now();
    let mut j = 0.0;
    for i in 0..1_000_000_000i64 {
        j += f(i);
    }
    println!("{:?}, {:?}", j, tbegin.elapsed().unwrap());
}

fn f(i: i64) -> f64 {
    return i as f64 / 3.1415926
}

Асинхронный код:

//[dependencies]
//futures = "0.3"
use futures::executor::block_on;

fn main() {
     block_on(async {
        let tbegin = std::time::SystemTime::now();
        let mut j = 0.0;
        for i in 0..1_000_000_000i64 {
            j += f(i).await;
        }
        println!("{:?}, {:?}", j, tbegin.elapsed().unwrap());
    });
}

async fn f(i: i64) -> f64 {
    return i as f64 / 3.1415926
}


Результаты

Мы видим, что арифметика у всех 3-х языков получается одинаково хорошо, а вот накладные расходы на await отличаются на порядок. Интересно, что там где использование async/await наиболее распостранено (и даже пропагандируется), издержки на асинхронный вызов просто запредельные. Выиграл гонку как всегда Rust, возможно это главная причина, по которой WEB-фреймворк, написанный на нем, стабильно выигрывает бенчмарки уже не первый год.


Резюме

Неспроста разработчики Java не торопятся добавлять асинхронный синтаксис непосредственно в язык, теперь возможно и я, обнаружив новое модное асинхронное API, задумаюсь.

Спасибо за внимание.

© Habrahabr.ru