13 января

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

Высокая производительностьJavaScriptПрограммированиеC#Rust

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


Работая с 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<number> {
    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<double> 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
}

Результаты


Язык Синхронный код (сек.) Асинхронный код (сек.) %% потерь
Typescript 7.48 173 23 раза
C# 7.46 76.2 10 раз
Rust 7.45 19.2 2.6 раза

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


Резюме


Неспроста разработчики Java не торопятся добавлять асинхронный синтаксис непосредственно в язык, и хотя я считаю, что async / await — прекрасная абстракция, но мы должны понимать масштаб накладных расходов при ее использовании.


PS
Спасибо всем, кто указал на возможность ускорить код с помощью кэширования тасков / промисов (вместо кэширования результатов), а также на наличие в C# прекрасного инструмента, как раз решающего мою проблему.

Теги:async/awaitzero-costrustnode.jsбенчмаркинг
Хабы: Высокая производительность JavaScript Программирование C# Rust
+4
12,5k 76
Комментарии 126
Реклама
Похожие публикации
Лучшие публикации за сутки

Рекомендуем

Разместить