Комментарии 75
Транспилируется оно в генераторы: https://babeljs.io/docs/plugins/transform-async-to-generator/
В чём именно просадка затрудняюсь сказать.
Производительность нативных асинхронных функций должна быть на уровне генераторов.
Даже из стека видно, что используются обещания из core-js
, а не нативные, а генераторы компилируются регенератором. Так что здесь вы тестируете совсем не производительность и отладку асинхронные функции и в том, что здесь babel генерирует весьма паршивый код виноваты только вы.
Уже поправил этот косяк. Время работы не изменилось.
Не удивлён, так как здесь складываются издержки приличного числа нативных (далеко не самых быстрых) обещаний и генераторов. А вот от кода babel
здесь остатся только оборачивание генератора в хелпер. При желании это хорошо оптимизуется альтернативными преобразованиями и оптимизированными обещаниями из bluebird
, как ниже писал ChALkeRx.
Время работы единичного вызова функции на три миллисекунды, большую часть из которых оно читает файл — вообще скорее показатель погоды на Марсе, так что обсуждать его изменения и не изменения довольно бессмысленно.
Раз вы знаете как надо правильно готовить babel, чтобы он не тормозил, то с нетерпением жду пулреквест.
Дискуссия async/await vs fibers началась несколько дней назад в коментах к моей статье Пишем микросервис на KoaJS 2 в стиле ES2017. Часть I: Такая разная ассинхронность. (название похоже или мне показалось :) ). Там эта цепочка не вызвала бурного обсуждения, но как я вижу, дискуссия не окончена.
Стандарт Async Functions будет принят в конце ноября, сейчас он в стадии 'Stage 3 ("Candidate")'. Это означает что Ваш код, написанный под этим синтаксисом меняться не будет и Вас нету рисков что-то переписывать.
- Неккоректно сравнивать быстродействие async/await в совокупности с транспайлером babel, так как это не самая быстрая реализация этого синтаксиса. Если использовать модуль asyncawait то, по утверждению автора модуля, производительность составлячет 79% от callback-стиля оформления кода. Другими словами потеря в скорости несущественная. Ситуация может измениться в нативной реализации, которая вот-вот, появится.
Огромное количество модулей уже описаны промисами, вам стоит просто написать await и ваше приложение ассинхронно подождет.
- То что у Fibers нету стандарта означает что синтаксис может претерпевать изменения, это существенно повышает риски долгосрочной поддержки такого кода.
Мне симпатична технология Fibers, но я не понимаю зачем ей противопоставлять async/await. Обе имеют свои плюсы и минусы.
Дискуссия async/await vs fibers началась несколько дней
А репозиторий создан в апреле. На самом деле этой дискуссии не один год.
название похоже или мне показалось :)
Это пасхалка :-)
Если использовать модуль asyncawait то, по утверждению автора модуля, производительность составлячет 79% от callback-стиля оформления кода.
Примечательно, что этот модуль — обёртка над node-fibers.
Огромное количество модулей уже описаны промисами, вам стоит просто написать await и ваше приложение ассинхронно подождет.
Лучше я напишу в одном месте Future.fromPromise( p ).wait(), чем буду по всему коду раскидывать async и await.
То что у Fibers нету стандарта означает что синтаксис может претерпевать изменения, это существенно повышает риски долгосрочной поддержки такого кода.
В том-то и дело, что он не добавляет никакого нового синтаксиса. Только апи для запуска и переключения волокон.
я не понимаю зачем ей противопоставлять async/await. Обе имеют свои плюсы и минусы.
Как я обожаю эту толерантность. Обе технологии выполняют одну и ту же функцию. Только одна прямо, хоть и не стандарт, а другая криво, хоть и почти-вот-вот-стандарт.
Примечательно, что этот модуль — обёртка над node-fibers.
Так это еще один плюс в копилку async/await. Смысл для такой "крутой техногии" даунгрейдится в "более примитивную". Кроме того, из обсуждения ниже понятно, что замеры производительности сомнительны даже для babel.
Лучше я напишу в одном месте Future.fromPromise( p ).wait(), чем буду по всему коду раскидывать async и await.
Сомнительная красота.
В том-то и дело, что он не добавляет никакого нового синтаксиса. Только апи для запуска и переключения волокон.
А что api меняться не может?
Как я обожаю эту толерантность. Обе технологии выполняют одну и ту же функцию. Только одна прямо, хоть и не стандарт, а другая криво, хоть и почти-вот-вот-стандарт.
Тут "толерантность" не совсем подходит, синергия в случає с модулем обёрткой над node-fibers.
У node-fibers есть сильные стороны, которые вы уже перечислили но есть проблемы:
Есть некий талантливый разработчик Marcel Laverdet, который это все кодит. Ему сейчас интересно, репозиторий живой, но сильная зависимость от одного разработчика это риск для технологии. Ведь не всем она нравиться и ваша голосовалка говорит о том что даже после ваших блестящих аргунетов лидирует async/await
Бинарный код модуля это тоже проблема, т.к. есть облачные хостинги и среды с ограниченными возможностями по компиляции, где Вы не сможете собрать node-fibers.
- Изомофный код, который становиться популярным в современных фреймворках не может использовать fibers, т.к. нету для него транспайлера.
Ситуация может измениться в нативной реализации, которая вот-вот, появится.
Уже есть под флагом в браузере (chrome 52). Плюс давно есть в Чакре.
Что такое "квазимногопоточность"?
Я извиняюсь, а вы с какой нагрузкой тестировали? Попробуйте запустить в 50 параллельных запросов от пользователя хотя бы, у вас числа в синхронном случае (и выводы) сразу изменятся.
Кроме того, не надо fs.readFile ручками в промис оборачивать, возьмите Bluebird.
Кроме того, вы под какой версией Node.js/v8 тестировали? В последнем релизе ещё v8 5.0.x, и там реализация Promise ещё медленная. В 5.3 стало получше (они занялись оптимизацией), но всё ещё не идеально. Пока что есть смысл делать const Promise = require('bluebird')
наверху каждого файла.
И да, вы код, полученный через Babel точно поверх нативных генераторов гоняли, а не поверх регенератора?
Плюс ваш тест слишком маленький, чтобы показать проблемы кода на каллбэках. Посмотрите в сторону https://github.com/petkaantonov/bluebird/tree/master/benchmark, например.
Я извиняюсь, а вы с какой нагрузкой тестировали?
Ни какой. Просто запускал код из статьи.
Попробуйте запустить в 50 параллельных запросов от пользователя хотя бы, у вас числа в синхронном случае (и выводы) сразу изменятся.
Не изменятся. Синхронный вариант так и останется быстрее асинхронного. Но при этом многозадачная реализация обслужит в секунду больше клиентов, чем однозадачная, благодаря более эффективному использованию процессорного времени.
Кроме того, вы под какой версией Node.js/v8 тестировали?
6.3.1
Пока что есть смысл делать const Promise = require('bluebird') наверху каждого файла.
Получается в полтора раза медленнее.
И да, вы код, полученный через Babel точно поверх нативных генераторов гоняли, а не поверх регенератора?
Вы правы, у меня использовался пресет es2015
, который содержал в том числе и регенератор. Оставил один transform-async-to-generator
плагин — по скорости стало даже ещё медленнее.
Плюс ваш тест слишком маленький, чтобы показать проблемы кода на каллбэках.
Это далеко не самая главная их проблема, чтобы заморачиваться огромными бенчмарками :-)
Синхронный вариант так и останется быстрее асинхронного. Но при этом многозадачная реализация обслужит в секунду больше клиентов, чем однозадачная, благодаря более эффективному использованию процессорного времени.
Что? Вы, видимо, под «быстрее» что-то другое имеете ввиду =).
Получается в полтора раза медленнее.
Слабо верится. Как конкретно вы измеряете? Можно увидеть код тестов?
Вы правы, у меня использовался пресет es2015, который содержал в том числе и регенератор. Оставил один transform-async-to-generator плагин — по скорости стало даже ещё медленнее.
И в то, что transform-async-to-generator в три раза медленнее генераторов, я тоже как-то плохо верю
Можете выложить все исходники и то, как вы их запускали/измеряли куда-нибудь?
Это далеко не самая главная их проблема, чтобы заморачиваться огромными бенчмарками :-)
Основная проблема — читабельность кода и удобство работы с ошибками, там это видно лучше, кмк.
Что? Вы, видимо, под «быстрее» что-то другое имеете ввиду =).
Время исполнения. А вы по всей видимости имеете ввиду пропускную способность.
Слабо верится. Как конкретно вы измеряете? Можно увидеть код тестов?
https://github.com/nin-jin/async-js
Каждый вариант в отдельной ветке.
Можете выложить все исходники и то, как вы их запускали/измеряли куда-нибудь?
Статья как бы усеяна ссылками на исходники. Запускаются через npm test
.
https://github.com/nin-jin/async-js
Спасибо.
Статья как бы усеяна ссылками на исходники.
Не заметил, извините.
Я не заметил замер времени у вас, скорее всего потому, что так замерять время ну вот вообще нельзя. Вы измеряете время одного выполнения функции, один раз и на холодную.
Да и хорошо бы слить их всё-таки в одну ветку и запускать по очереди.
Сравнил.
async function test() {
await greeter.say('Hello', user)
await greeter.say('Bye', user)
}
async function app() {
const start = process.hrtime();
for (var i = 0; i < 1000; i++) {
await Promise.all(new Array(100).fill(0).map(test));
}
console.error(process.hrtime(start));
}
Bluebird более чем в два раза быстрее получился. stdout пайпил в /dev/null, если медленный вывод убрать и в greeter делать просто await в экспортируемую переменную (чтобы не соптимизировать) — то Bluebird быстрее более чем в три раза.
На вашем тесте с одиночным вызовом — да, Bluebird получается чуть медленнее, но это вот вообще ни о чём не говорит. Как и остальные результаты ваших тестов, впрочем.
Замерьте как считаете правильным :-) Статья вообще не о производительности.
См. выше как правильно.
Статья вообще не о производительности.
Ой, а почему 4 из 4 пунктов выводов касаются производительности и делают какие-то утверждения на основе ваших бенчмарков?
Upd: а, это выводы секции «производительность». Но всё равно при прочтении беглом остаётся такое впечатление, что вы тут говорите, что с async/await и промисами всё медленно и печально, поэтому давайте будем использовать не их, а вот это.
См. выше как правильно.
Отлично, сравните теперь с волокнами.
Ой, а почему 4 из 4 пунктов выводов касаются производительности?
Потому что они находятся в разделе "Производительность".
Отлично, сравните теперь с волокнами.
Дайте аналог
async function test() {
await greeter.say('Hello', user)
await greeter.say('Bye', user)
}
async function app() {
const start = process.hrtime();
for (var i = 0; i < 1000; i++) {
await Promise.all(new Array(100).fill(0).map(test));
}
console.error(process.hrtime(start));
}
на волокнах — сравню. Чтобы вы не говорили, что я всё не так делаю =). На всякий случай — тут мы 1000 раз запускаем по 100 условно параллельных запросов и каждый раз (из 1000) ждём, пока все эти 100 запросов выполнятся.
Потому что они находятся в разделе "Производительность".
Уже заметил и даже успел поправить комментарий до вашего ответа =).
function test() {
greeter.say('Hello', user)
greeter.say('Bye', user)
}
function app() {
const start = process.hrtime();
console.time('time')
for (var i = 0; i < 1000; i++) {
Future.wait(new Array(100).fill(0).map(test.future()));
}
console.error(process.hrtime(start));
}
Future.task( app ).resolve( error => {
if( error ) console.error( error )
} )
О, не успели. Хотя сейчас проверю и это.
А так медленнее.
async/await (через babel) — 1.1 секунды, fibers — 1.6 секунд.
Edit: стоп, он расширяет прототип Function?
А как быстрее? У меня волокна за 1.2 отрабатывают, а то, что babel генерирует — за 2.8.
К сожалению, да.
Я же написал — вставьте везде const Promise = require('bluebird')
. Наверху каждого файла, даже там, где вы руками Promise
не вызываете — его вызывает babel.
В greeter.js:
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));
let config;
const getConfig = () => {
if (config) return config;
return config = fs.readFileAsync('config.json').then(x => JSON.parse(x));
}
C then — чтобы код был аналогичен вашему в другом месте, но можно и на async переписать:
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));
let config;
async function getConfig() {
if (config) return config;
const configText = await fs.readFileAsync('config.json');
return config = JSON.parse(configText);
}
Так неоптимально, потому что текст не кэшируется и мы реально первые 100 раз читаем файл, но это аналогично тому, что вы c fibers написали. По времени получится 1.3 сек, что всё равно быстрее чем fibers (1.6 сек).
В Babel включаем пресет 'stage-3' и всё. Или плагин 'transform-async-to-generator' и всё.
Кстати, попробуйте с числами 1000/100 поиграться.
У меня такое ощущение, что fibers очень быстрый, когда асинхронность не нужна (то есть когда условное «волокно» одно), но когда их много — он очень сильно сливает.
На одинаковом неэффективном коде getConfig (см. выше), чтобы не давать никому преимущество, сравнивайте первые две позиции.
- 100000 раз * 1 параллельный:
async/await (babel) — 1.5 сек
fibers — 0.7 сек
async/await (babel), с правильным кэшем файла — 1.4 сек - 10000 раз * 10 параллельных:
async/await (babel) — 1.3 сек
fibers — 1.4 сек
async/await (babel), с правильным кэшем файла — 1.1 сек - 1000 раз * 100 параллельных:
async/await (babel) — 1.3 сек
fibers — 1.6 сек
async/await (babel), с правильным кэшем файла — 1.1 сек - 100 раз * 1000 параллельных:
async/await (babel) — 1.7 сек
fibers — 9.4 сек
async/await (babel), с правильным кэшем файла — 1.4 сек - 25 раз * 4000 параллельных:
async/await (babel) — 3.4 сек
fibers — 85.4 сек
async/await (babel), с правильным кэшем файла — 1.6 сек
Я правильно понимаю, что если обработку запросов пихать в fibers, то оно очень быстро развалится при увеличении кол-ва одновременных запросов?
Обратите внимание, что async/await в целом стабилен — ему не важно, какая у вас геометрия.
Да, забыл сказать: после 4000 он начинает ещё сильнее разваливаться, и там явно нелинейная прогрессия — я просто не дождался пока он выполнится.
Upd: убедился, что дело не в console.log
— плющит и без него.
За что я обожаю синтетические тесты, так это за то, что можно получить любые нужные результаты.
const REQUESTS = 100
const MEASURES = 1000
const DEEP = 10
const CALLS = 10
var Future = require( 'fibers/future' )
function stay() {
var future = new Future
setImmediate( ()=> future.return() )
return future
}
var test = (() => {
function inner(j) {
return ( (j > 0) ? inner(j - 1) : stay().wait() )
}
for (var i = 0; i < CALLS; ++i) {
inner(DEEP)
}
})
function app() {
const start = process.hrtime();
for (var i = 0; i < MEASURES; i++) {
var tasks = new Array(REQUESTS).fill(0).map(()=>Future.task(test))
Future.wait( tasks );
tasks.map( task => task.get() )
}
console.log(process.hrtime(start));
}
Future.task(app).resolve( error => {
if( error ) console.error( error )
} )
7s
const REQUESTS = 100
const MEASURES = 1000
const DEEP = 10
const CALLS = 10
const Promise = require('bluebird');
function stay() {
return new Promise( resolve => {
setImmediate( ()=> resolve() )
})
}
async function test() {
async function inner(j) {
return await ( (j > 0) ? inner(j - 1) : stay() )
}
for (var i = 0; i < CALLS; ++i) {
await inner(DEEP)
}
}
async function app() {
const start = process.hrtime();
for (var i = 0; i < MEASURES; i++) {
await Promise.all(new Array(REQUESTS).fill(0).map(test));
}
console.log(process.hrtime(start));
}
app().catch( error => {
console.error( error )
} )
25s
У вас в этом коде ничего асинхронного не происходит вообще — стоит проверять на чём-то более реальном, чем пустой new Promise
вокруг setImmediate. Хочу заметить, что в прошлый раз код и пример целиком был ваш, и я только варьировал нагрузку (кол-во параллельных запросов), и хуже всё было при > 5 уже.
Но даже так, например на 500/100/5/5 — Promise-версия выигрывает в два раза.
И это даже не самое главное, главное то, что при этом fibers-версия периодически падает на вашем коде с
./node_modules/fibers/future.js:471
}).run();
^
RangeError: Maximum call stack size exceeded
То есть оно вообще не работает.
Поставьте 500/10/1/1 (последние можно как угодно выбирать, по 1 просто для того, чтобы быстрее отработало, да и MEASURES уменьшил просто для ускорения, падает и с вашим тоже) и запустите
for i in `seq 100`; do node libs/fib.js > /dev/null; done
У меня где-то в 20% процентов случаев оно тупо падает.
У вас в этом коде ничего асинхронного не происходит вообще
setImmediate — простейшая асинхронная функция.
Хочу заметить, что в прошлый раз код и пример целиком был ваш, и я только варьировал нагрузку (кол-во параллельных запросов), и хуже всё было при > 5 уже.
В прошлый раз вы увеличивали число задач, не меняя числа функций. Пенальти от волокон пропорционально числу задач. А пенальти от генераторов пропорционально числу запусков функций. Всё, что я тут сделал — это увеличил число запусков функций и промисы вновь стали медленнее. Играясь с коэффициентами можно добиться любого результатата.
На мой взгляд более правдоподобны следующие коэффициенты:
const MEASURES = 20
const REQUESTS = 1000
const DEEP = 10
const CALLS = 10
Они дают 9с для генераторов и 7с для волокон.
И это даже не самое главное, главное то, что при этом fibers-версия периодически падает на вашем коде с
У меня падает с переполнением стека лишь при DEEP > 16000. Версия на генераторах падает уже при DEEP > 1300.
setImmediate — простейшая асинхронная функция.
У вас нету никакого асинхронного ввода-вывода и ничего нигде не ждёт. setImmediate
сразу же добавляет каллбэк в очередь, ваш код из примера можно переписать на полностью синхронном, добившись такого же порядка выполнения руками. Тогда скорость ещё больше будет, но кому это нужно в реальном мире?
На мой взгляд более правдоподобны следующие коэффициенты:
На всякий случай — у вас поменялся порядок констант, так что это 1000/20/10/10 в той форме, как я выше записывал (чтобы никто не запутался).
И да, у меня на ноуте — 9 с для async/await и 11 сек для fibers на этих параметрах. Но это даже не особо важно, см. ниже.
У меня падает с переполнением стека лишь при DEEP > 16000. Версия на генераторах падает уже при DEEP > 1300.
Запустите 100 раз, как я написал выше — оно рандомное. DEEP
и CALLS
поставьте в 1, дело вообще не в них. Хотя падает и с 10/10, просто работать дольше будет, если не упадёт. REQUESTS
поставьте в 500 как у меня или в 1000 как у вас. MEASURES
поставьте в 10 или 20 как у вас.
Выкинул всё явно лишнее, вот код (получен из вашего разворачиванием циклов и рекурсии для DEEP=0 и CALLS=1):
const REQUESTS = 1000
const MEASURES = 2
const Future = require('fibers/future');
function stay() {
const future = new Future;
setImmediate(() => future.return());
return future;
}
function test() {
stay().wait();
}
function app() {
for (var i = 0; i < MEASURES; i++) {
const tasks = new Array(REQUESTS).fill(0).map(() => Future.task(test))
Future.wait(tasks);
tasks.map(task => task.get());
}
}
Future.task(app).resolve(error => {
if (error) console.error(error);
});
Запускать так:
for i in `seq 100`; do node fibfail.js; done
У меня упало 24 раза из 100 запусков.
Есть идеи откуда может браться эта недетерменированность?
Честно говоря, мне сейчас немного не до того, чтобы в fibers баги чинить, извините. По недерменированности — банально рейсы в fibers какие-нибудь, например. Я его код не смотрел, не могу точнее сказать =).
Да и репортить проблему в fibers я не буду, потому что это ваш код и я даже не вникал в его правильность =).
Но могу сообщить информацию об окружении и прочие детали, если у вас проблема не воспроизводится.
Если вы думаете, что это проблема самого Node.js — приносите тесткейс без fibers, посмотрим.
Знаете, решил на его тесты посмотреть.
https://github.com/laverdet/node-fibers/blob/master/test/stack-overflow2.js вообще стабильно (100%) сегфолтится.
Может, ну его?
По поводу примера на основе вашего кода — с REQUESTS = 3000 валится в 90% случаев уже.
Упрощённый код:
const Future = require('fibers/future');
function test() {
const future = new Future;
setImmediate(() => future.return());
future.wait();
}
function app() {
const arr = new Array(3000).fill(0);
const a = arr.map(() => Future.task(test));
Future.wait(a);
const b = arr.map(() => Future.task(test));
Future.wait(b);
}
Future.task(app).resolve(error => {
if (error) console.error(error);
});
Наличие или отсутствие a.map(x => x.get())
и b.map(x => x.get())
ни на что не влияет.
В каком окружении вы всё это запускаете? Какая ось? Версия ноды? Архитектура процессора?
Только что проверил на VPS с Debian Jessie и тот же самый архив с офсайта — stack-overflow2.js
сегфолтится так же, тест выше не падает, но там рейс может вполне от мощности машины зависеть в том числе. Возможно, если покрутить числа — тоже упадёт, но я сейчас этим заниматься не буду.
Успехов в поиске бага, серьёзно =).
Если вдруг кто сюда придёт потом — бага зарепорчена в https://github.com/laverdet/node-fibers/issues/299.
Цитируя автора библиотеки, кстати:
for new projects I think it's the right choice to use those new language features over Fibers.
Так, написал, как получилось — действительно, быстрее вышло. Хотя стоит учесть, что реальном коде ввода-вывода куда больше чем одно чтение из файла на кучу вызовов, которое, к тому же, закэшировано.
1) чтобы таймеры выполнялись один за другим
2) выполнялись параллельно
Никак, это нативный модуль, и в браузере он работать не будет.
Хотя если считать https://tonicdev.com/npm/fibers js-песочницой, то можно =).
Оно на стороне сервера выполняется.
А написать красивый код без прерываний можно и на callback'ах, благо инструментов достаточно, однако я всячески поддерживаю инициативу в виде async/await.
Такая разная асинхронность