llvm-ir — это внутреннее представление кода, которое может быть скомпилировано в машинный код для целевой платформы (либо JIT-исполняться).
WASM — это тоже самое.
Как я понимаю, идея WASM, что он переносимый
Да, как и llvm-ir. Но переносимость это главное — главное изолированность.
Который затем должен быть интерпретирован/JIT-компилирован/компилирован целиком перед запуском в реальный машинный код платформы, на которой он запускается, верно?
Ну интерпретацией его никто заниматься не хочет. А так да — как ir.
В статье речь идет о запуске WASM в том числе и вне браузера.
Ну да, для целей похожих на броузер. Платформонезависимость + изолированность. Два кита.
если и так есть имплементации JVM почти под что угодно
Ну не под что угодно, а под конкретный набор платформ. Это просто бинарник, который не исполняется в изолированной среде. Ваша jvm может делать с системой что угодно.
А далее вам понадобится изоляция, а ещё и платформо-независимая. И вы сделаете очередную vm для нативного кода. Но тут всё сделано уже за вас.
и наше jvm-приложение таким образом УЖЕ переносимо?
Нет, вашего jvm-приложения на уровне платформы не существует — они никому не интересно. Это просто какая-то локальная логика jvm. Приложением является jvm и она не переносима.
К тому же, кейсы использования wasm«а выходят за „одно приложение“. Там обычно миллионы приложений. jvm/.net/js/python — тысячи их. Везде и всюду какие-то непонятные окружения, бинарники, платформа-специфичная структура.
WASM же един. Все программы имеют одинаковое окружение, а уже внутри этого окружения организуется необходимое вам окружение и там уже работает ваш конечный софт(если это какая-нибудь java).
Профит очевиден. Юзеру ненужно ставить себе тысячи рантайма тысяч языков. Вам ненужно собирать свой рантайм под тысячи платформ, чтобы таскать его с собою. Юзеру ненужно заботится о том, что ваша jvm может быть заражена и украсть его данные/сломать ему систему.
Он просто ставит заранее надёжный wasm-рантайм а далее его ничего не волнует. Что там у вас, как там у вас. Он просто использует что угодно, где угодно, безопасно.
Я не имел в виду поддержку GC непосредственно в WASM, я имел в виду как раз
Правильно. Если вам нужен ГЦ в программе на С++ — вы пишите гц на С++. Это не вызывает никаких проблем и С++ ненужна нативная поддержка гц. Васм такой же С++.
Очень даже есть. Например, .NET GC реализован как часть CoreCLR (если говорим о .NET Core), написан на C++, как большинство CoreCLR.
Ну С++ — это внешний для .net мир. Я имел ввиду нативно в контексте .net. Ведь именно потому гц написан там на С++.
Именно поэтому .net и нужен С++ и нативнища, ведь байткод и vm там высокоуровневые. Wasm же — это совершенно другой случай.
Это, конечно же, очень круто, но только эта платформа не аппаратная, абстрактная виртуальная машина.
llvm-ir тоже абстрактная виртуальная машина. Ключевым тут является не название, а смысл. Какого уровня эта машина и насколько полно она может обобщать эту аппаратную платформу.
И есть WASM-runtime, который занимается исполнением всего этого дела. Поэтому как не крути, это медленнее.
WASM-runtime не занимается исполнением кода. Он компилируемый, как и ir. Да, этому коду(как и ir) требуется некий рантайм, но в чём проблема? Никто и не говорил, что это бесплатно.
К тому же, вы предлагали(насколько я понял) заменить это жава/дотнет байткодом, который ещё более медленный, на порядки более медленный. И для решения этой проблемы там есть специальный jit, который воспринимает этот байткод не как байткод, а как некий язык высокого уровня.
ir и, в меньшей степени, wasm — уже оптимизируются на уровне байткода и транслируются в бинарник почти 1в1.
И идея подложить слой абстракции под абстракцию кажется мне весьма сомнительной.
Вы воспринимаете jvm/.net по своему, но всё это лишь обычные программы на том же C (C++/Rust/etc).
И этот слой нужен для изоляции, ведь ОС так же вносит множество оверхеда, как и все аппаратные средства изоляции в железе(процессоре). На самом деле да, в какой-то мере wasm и броузер заново изобретают ОС и работают хуже. Взять ту же изоляцию памяти. Но броузер не может стать ОС потому что уже есть ОС, есть вендорлок и никто никуда не уйдёт и никого никуда не пустят.
К тому же изоляция уровня ОС достаточна слаба и броузер(либо какая-то другая песочница) всё равно нужен и это так же будет накладывать множество ограничений. В том числе и по производительности.
В таком ключе я вижу целевое применение WASM исключительно в переносимом производительном коде, написанным на C/C++/Rust/etc. Что, имхо, довольно ограниченная ниша.
jvm/.net — это такой же С++ код как и любой другой.
И в этом его смысл. Мы можем в броузере(в какой угодно другой песочнице) запустить любой C/C++/Rust/etc-софт, а это значит, что мы можем запустить и jvm и v8 и что угодно. Да, это будет медленнее, но.
Смысл в том, что это будет работать и работать хорошо. Причём максимально безопасно. Вы сможете загрузить любой нужный вам рантайм к пользователю в броузер и делать так что хотите. Какая вам разница с того, что нативная jvm будет работать быстрее? Считайте, что у пользователя просто процессор похуже.
Хоть жс и хорош, но он был ошибкой. Причины тому просты — никто не думал, что это выльется в полноценные приложения на жс. Броузер стал некой новой ОС и такой высокоуровневый язык очень плох как базовый язык платформы.
потом сделали WebAssembly
Правильно. Это попытка исправить проблему.
чтобы запускать его быстрее
Быстрее — это просто следствие. Задача была «сделать максимально близкое к железу представление, которое может всё то — что может железо».
А потом мы захотим писать на чем-нибудь более удобном, чем C/C++/Rust/итп
А это неважно. Тут фишка в том, что вы на си можете написать питон, а на питоне си нет.
например, хотим сборку мусора
Мы просто берём и пишем любую сборку мусора.
и притащим GC поверх WASM
Нет. Ненужно ничего тащить. Вы, скорее всего, неправильно поняли. Когда говорят «тащить гц в васм» — это говорится именно про доступ из васма к js-объектам. Т.е. налаживания коммуникации между двумя отдельно запущенными в броузере vm. Некий такой ffi.
Но вам ничего не мешает собрать v8 под васм и запустить в броузере. Можете jvm собрать. Оно, конечно, так просто не соберётся т.к. использует очень много платформо-специфичных решений. И если их убрать/заменить на васм-специфичные, то никаких проблем не будет.
когда в .NET/JVM/итп он реализован нативно.
Нету там никакого нативно и в этом проблема. wasm — это именно llvm-ir, а не жава/дотнет-байткод. Об их фундаментальной разнице я говорил выше. Ни на каком жава/дотнет-байткоде вы jvm не напишите, а вот на ir/wasm вполне.
В этом фишка. Вы берёте базовый язык — это такой язык, который может «всё». А далее пишите что хотите, хоть jvm, хоть питон.
Тут уже сделали Blazor — веб-фреймворк на C#, путем портирования Mono на WASM.
Да, это так и работает. Так и задумывалось. Люди правильно сделали.
Мы портировали виртуальную машину на виртуальную машину, чтобы ты мог запускать виртуальную машину, пока запускаешь виртуальную машину (с).
Нет. Вы путаете слова и понятия. vm в разных контекста значит совершенно разное, ведь любая ОС — это по-сути vm.
Воспринимайте wasm как ОС + некую отдельную платформу(аппаратную). И уже далее на этой ПА платформе вы запускаете нативные программы. Такой нативной программой является mono. И люди её запустили. А ваш C#-код и его окружение — это уже ваши внутренние с .net/mono заморочки. wasm это не интересует, как и любую другую платформу(нативную или около того).
чем оно будет отличатся от имеющихся технологий? (.net, JVM).
Намного эффективнее(он более низкоуровневый), нет привязки к рантайму(гц, операции над объектами и прочее).
Или даже clang умеет генерить не привязанный к конкретной архитектуре код (флаг -emit-llvm )
llvm не совсем умеет отвязывать ir от архитектуры. База там действительно общая, но существует много всякой платформо-специфичной атрибутики. В целом ir почти во всём лучше wasm, но.
Основная фишка wasm — это именно изолированное(и эта изоляция намного ниже, нежели в ситуации с той же java) исполнение кода. Очевидно, что брать и генерировать asm — это не лучшая идея. Сам же llvm хоть и называется vm — vm не является.
Т.е. wasm — это такое безопасный ассемблер, который исполняется в вм. Он компилируемый, простой, крайне низкоуровневый, но он не позволяет читать/писать куда не надо. Это его основная задача и фундаментальное отличие от всего остального.
запуск задач по расписанию и другие системные задачи.
Что за глупости? Какое отношение все это имеет к ядру?
Выделение и освобождение памяти происходит с помощью вызовов malloc и free соответственно
Нет. В линуксе(да и везде) маллок/free не занимаются памятью, а работают только с виртуальным адресным пространством.
которые в итоге будут обработаны системными вызовами ядра, а значит отобразятся в утилизации CPU как системное время.
Не всегда, очень даже не всегда.
В большинстве современных операционных систем
К операционным системам это не имеет никакого отношения. Они лишь использует механизмы предоставленные железом.
виртуальная память организуется с помощью страничной адресации
Не виртуальная, как минимум не только виртуальная.
при таком подходе вся область памяти делится на страницы фиксированной длины
Память.
и при выделении, например, 2 Гб памяти, менеджеру памяти придётся оперировать более чем 500000 страниц
Нет. Менеджер памяти тут вообще не при делах и работает он, как я уже сказал, с адресным пространством. Т.е. он выделит просто диапазон и повесит на него права.
Отображением занимаются совершенно другие подсистемы. Причём занимаются только только созданием этих отображений — далее процессор всё делает сам.
В таком подходе появляются большие накладные расходы на управление и для их уменьшения были придуманы технологии Huge pages
Не для этого.
с их помощью можно увеличить размер страницы
Это аппаратная фича.
например до 2МБ, что существенно сократит количество страниц в куче памяти.
Не для этого.
В некоторых случаях THP вызывает ничем не мотивированное увеличение потребления CPU в систем.
Не в некоторых, а вполне в конкретных. И всё там мотивировано.
В заключение хотелось бы дать несколько советов, для успешного поиска проблем с производительностью:
Ничего из этого сделано не было. Да и советы достаточно глупые. Абстрактная нагрузка на процессор — ничего не показывает. Нужно использовать нормальный профайлер и собирать адекватный трейс, а уже далее смотреть.
Дак для чего же нужны эти большие страницы? Всё очень просто. Абсолютно неважно — сколько там страниц, пока они влезают в кэш. Есть специальный кэш — называется tlb. Когда происходит промах(т.е. отображения нету в кэше) — процессор вынужден пойти(автономно — ЭТО НЕ СИСТЕМНОЕ ВРЕМЯ) — найти и прочитать это отображение из памяти(если не нашел — тогда уже триггерится пейджфолт и уже тогда включается ядро).
Если отображение уже существует в памяти, то это на трупут почти не влияет. Процессор умеет в префетч и заранее подготавливает все отображения. Да, это съедает чуть-чуть трупута памяти(т.к. отображения читать из памяти нужно), но это крохи.
Но проблемы возникают именно на random access, когда никакой префетч не работает и трупут упирается в летенси(помноженной на блок) памяти. Поиск отображения — очень дорогая по летенси операция и от того задержки памяти возрастаю в разы.
Для предотвращения подобного поведения и нужны большие страницы. Их нужно меньше и в кэш влезают уже не мегабайты, а сотни, тысячи мегабайт памяти.
У этого подхода существует только одна проблема — это разряженная память. Дело в том, что по-умолчанию ядро попросту разрешает r/w в какой диапазон вадресспейса. Памяти в «памяти» выделяемой маллоком нет. Вообще(если это большие блоки(100к и больше по умолчанию, насколько я помню)).
Далее, когда мы начнём читать/писать в память — процессор не найдёт отображения и сгнерирует пейджфолт. Далее ядро пойдёт и в специальную таблицу(дерево) запишем отображение. Далее процессор всегда будет читать его или из памяти или из кэша(тлб).
Если мы будем читать/писать память байт за байтом, то первое чтение затригеррит пейджфолт и ядро свяжет первую(нулевую) страницу нашего отображения с реальной страницей(физической памяти). Для последующих 4096 байт(включая текущие чтение/запись) память будет. После — её не будет и опять будет пейджфол.
Тут уже можно понять — в чём проблема. Если мы выделим 2 мегабайта памяти и запишем половину — в ситуации с 4к страницами — выделится 1 мегабайт физической памяти. В ситуации с 2м страница — 2 мегабайта. Подобная ситуация сплошь и рядом в том же С++.
Тоже самое с разряженными массивами. Если у нас есть гигантский массив и мы сделаем туда 10 записей — мы затриггерем в среднем 10 пейджфолтом и свяжем 10 страница по 4к. Если тоже самое мы сделаем с 2м страницами — будет 10 по 2м.
Откуда может взяться высокий systime? Всё очень просто. Пейджфолты сами по себе дорогие, но разницы между типа страниц почти нет. Но.
Дело в том, что ядро при связывании страницы ОБЯЗАНО её забить нулями, т.е. пройтись мемсетом. Очевидно, что если мы в С++ запишем в строку 1гб + 1 байт, то ядро сделает мемсет на 1гб + 4к в случае с 4к страницами и 1гб + 2м в случае с 2м. И 1гб + 1гб в случае с 1гб страницами. Этим же обусловлено и возросшее потребление памяти.
И если в софте очень много разряженной памяти, то очевидно — ему повсеместные 2м+ страницы будут вредны, а в современном железе там 1гб и более страницы.
Да, именно поэтому вы не можете написать ваш код на голом хаскеле без либ, а я могу(написать аналогичный код на С/С++). Причина тому проста, и вы сами её где-то признали, что ваш массив и операции над ним — являются читами, а каноничным у вас является лишь интерфейс, который лишь создаёт видимость чистоты, ленивости и всего остального. Про соотношение массива и гц — я уже говорил.
Речь шла о том, сколько надо думать при написании кода. Даже если написание сводится к переписыванию готового и отлаженного алгоритма с другого языка.
Нет, речь шла не об этом. Речь шла о том, что вы вначале написали РАБОЧИЙ вариант на хаскеле(так, как привыкли), а далее началась борьба с его проблемами. И вы победили, в данном случае.
На С++ вы написали такой же РАБОЧИЙ вариант, но ни с чем вам воевать не пришлось. В этом разница.
Мне показалось, что у вас сложилось впечатление, будто действительно сначала пишется код с IntMap, а потом уже как-то там оптимизируется и переписывается. Это не так.
Ну почему же. Вы по-сути сделали нам ретроспективу вашей победы. В любом случае, абсолютно неважно что там — массив/не массив. Всё остальное никуда не уходит. Я не знаю как видите вы, но не могу верить вам. И я не сомневаюсь в вашей честности, просто человеку свойственно обманываться в пользу своих убеждений.
Питон, на который вы сослались.
Зачем вы врёте? Я не ссылался на питон — это вы на него ссылались. Я говорил о конкретных свойствах языков, которые пройдут этот тест — питон ему не соответствует. И вы его привели как контр-пример моим тезисам, но это не было контр-примером. Далее в своих рассуждениях на тему «почему примерно равная производительность равна»(в данном случае).
Почему Array что-то там взламывает?
Да, я уже много примеров приводил. гц/операции над массивом. Т.е. массив по-умолчанию мутабельная структура и из неё нельзя сделать иммутабельную структуру(дёшево). Ваши операции мутабельные, массив по-сути является множеством значений(переменных), что так же не особо канонично. Вы сам в этом признавались, что если бы не «готовые функции» — никакой массив я бы использовать не мог.
Вот возьмите, пожалуйста, массив. Без использования готовой функции(которая взламывает вашу концепцию) инициализируйте его своим алгоритмом.
Только код, который я пишу, остаётся чистым. Целиком. Мне не нужно пачкаться о состояние. А что там в кишках или в процессоре при этом происходит — десятое дело.
Нет, это ошибка. Ведь мы говорим об уровне языка и концепций. То, как именно пишите вы — мало на что влияет. Ведь я тоже могу писать на С++ безопасной и мой код будет безопасным, но концептуально это ничего не изменит, как и не изменит С++.
Это не матчасть, это что-то, имеющее вполне ощутимые эффекты. Например, атомики и CAS на чистом языке реализовать чуть менее тривиально, потому что у объектов больше нет адреса, их характеризующего.
Это ограничения сверху. А ограничения сверху — это то, что матчасть волнует мало. Низ первичен — верх вторичен. Низ диктует условия — верх подчиняется. Низ прорывает абстракции верха, но низу насрать на абстракции верха.
Поэтому да, у вас нету постоянных адресов и это накладывает какие-то ограничения на вас. Но это никак не влияет на матчасть. Для матчасти ваши ограничения — лишь один из множеств возможных вариантов.
Нет. В любом другом мейнстримном языке, будь то Java, C++, Go, Rust или Julia, если у вас есть переменная типа int, то в ней лежит int, а не указание, как этот int вычислить, если вдруг понадобится.
А причём тут «как вычислять»? «как вычислять» — это ваша локальная специфика, название уровня ваших абстракций. На уровне матчасти никаких «как вычислять нет», а есть код и данные, да и даже между ними разницы нет и код тоже данные.
Поэтому, что там навешано на объект — это неважно, неважно как оно называется на уровне абстракций.
Тут было бы иронично вспомнить про SSA в процессе компиляции, но то такое.
Тут никаких противоречий нет.
Это свойства конкретной структуры данных и её взаимодействия с компилятором, но не её семантика. Хотя, может, мы по-разному определяем семантику, хз.
Это именно семантика, в вашем случае. Ведь нужен был не просто массива, вы вкладывали в него иной смысл. И все вкладывают в него иной смысл. Возможно за вас кто-то уже вложил в используемый вами массив нужный смысл и именно этим вызвано ваше непонимание моих рассуждений.
Просто подумайте. У вас есть массив с value-типом. Подошел ли вам массив другой? Не с value-типом. Он бы соответствовал определённой вами семантики, но нет — он бы вам не подошел и вы бы искали другой. Потому что на этом уровне семантика чуть шире базовой.
А, кстати, как строить графички на плюсах, вы так и не рассказали, увы :(
Не знаю. Я не занимаюсь графиками. Если что-то нужно — использую упомянутый выше gnuplot, либо какой-нибудь онлайн-сервис. Но нужно это мне это крайне редко.
Попробовал переключать на always, never и обратно, но отличия были в пределах погрешности. Или нужно было как то иначе их включать?
cat /proc/meminfo, perf stat -dddd Можете написать сюда выхлоп — разберёмся.
Вообще, я думаю, что в данном случае все немного проще.
Ну дак у меня же всё меняется(при переключении), практика соотносится с теорией.
Обслуживание ~30000 горутин априори чуть более затратно, чем четырех горутин + chan.
Это копейки на фоне всего остального. Там ниже есть мой код — там можно передать кол-во потоков. У меня на 30000потоков(на самом деле там создаётся далеко не 30к потоков, а гораздо больше) это добавляет 1-2секунды к 50 имеющимся. И это потоки ОС.
Однако из-за того что я тестирую это на процессоре 7 летней давности эта разница кажется куда более заметной для меня.
Ну я вижу с never такую же разницу, которую видите вы. Эта разница ничем, кроме влияния thp, не объясняется. По крайней мере ничем предложенным вами.
66,468976022 seconds time elapsed - go mt
64,819841304 seconds time elapsed - go
64,380398227 seconds time elapsed - go v3
64,001787006 seconds time elapsed - hs
62,100266396 seconds time elapsed - c++ + gcc
47,760004537 seconds time elapsed - c++ mt + gcc
51,243138067 seconds time elapsed - c++ mt + clang//я проверил множество раз
80 sec - nodejs(32 бита числа)
67,263350647 seconds time elapsed - rust(rustc 1.33.0)
50,179576857 seconds time elapsed - rust mt
47,933258835 seconds time elapsed - rust mt + taskset
Странно, что результат так сильно от процессора зависит.
Там дело не совсем в процессоре.Я уже писал про transparent huge pages.
У вас «многопоточность» не код ускорят, а tlb-промахи. Т.е. если отображения нету в кэше(tlb) — процессор идёт искать отображение в памяти. В случае с одним потоком — ядро(процессор) почти всё время(половину) занимается поиском. В многопотоке этот поиск размазывается по ядрам.
75,197948177 seconds time elapsed// - ваш
124,388672662 seconds time elapsed// - который "однопоточный"
66,385085639 seconds time elapsed// ваш на всех потоках.
58,259779411 seconds time elapsed// ваш, все ядра и thp
Нужно искать людей с amd, либо с e5/2011 платформой. Там должен быть профит куда сильнее. На худой конец можно на амазоне тачку снять и погонять бенчмарки.
Там ненужно много ядер — оно уже двух перестаёт расти, а на 4х начинает деградировать.
Ускорение на четверть — это ж адски плохо для такого алгоритма.
Нет, с чего вдруг? Алгоритм говном — для него всё плохо.
И вот тут бы у вас, кстати, плюсы засияли, потому что вы из разных тредов в один std::vector писать можете
Нет. Дефолтная десктопная/e3 платформа штеуда утилизирует практически 70-80 трупута памяти. Поэтому никакие наивные представления про ускорение — здесь не прокатывают. Да, код подобного уровня никогда не снимет даже 10-20% трупута памяти, поэтому и существует какие-то наивные оценки на тему.
Но тут есть одна проблема. Из-за того, что алгоритм говно — получается так, что он съедая 10-20% трупута — съедает почти весь.
Причина заключается в следующим: кэш не умеет читать память байтами, 4 байта, 8 байтами, даже 32 не умеет — только кешлайнами, а это 64 байта.
Поэтому, при RA в память в подобном кейсе на каждое чтение int64_t на самом деле читает 64 байта, то же самое и с записью. Именно поэтому и наблюдает такое поведение. Код говно, алгоритм говно, а каналы до памяти все сожраны.
К тому же. не стоит забывать, что атомики сами по себе имеют большой оверхед и сравнивать надо не с дефолтным вариантом, а с запуском на одном треде(второй аргумент там — кол-во тредов). Там, скорее всего, будет секунд 100(если не больше) — я не проверял.
WASM — это тоже самое.
Да, как и llvm-ir. Но переносимость это главное — главное изолированность.
Ну интерпретацией его никто заниматься не хочет. А так да — как ir.
Ну да, для целей похожих на броузер. Платформонезависимость + изолированность. Два кита.
Ну не под что угодно, а под конкретный набор платформ. Это просто бинарник, который не исполняется в изолированной среде. Ваша jvm может делать с системой что угодно.
А далее вам понадобится изоляция, а ещё и платформо-независимая. И вы сделаете очередную vm для нативного кода. Но тут всё сделано уже за вас.
Нет, вашего jvm-приложения на уровне платформы не существует — они никому не интересно. Это просто какая-то локальная логика jvm. Приложением является jvm и она не переносима.
К тому же, кейсы использования wasm«а выходят за „одно приложение“. Там обычно миллионы приложений. jvm/.net/js/python — тысячи их. Везде и всюду какие-то непонятные окружения, бинарники, платформа-специфичная структура.
WASM же един. Все программы имеют одинаковое окружение, а уже внутри этого окружения организуется необходимое вам окружение и там уже работает ваш конечный софт(если это какая-нибудь java).
Профит очевиден. Юзеру ненужно ставить себе тысячи рантайма тысяч языков. Вам ненужно собирать свой рантайм под тысячи платформ, чтобы таскать его с собою. Юзеру ненужно заботится о том, что ваша jvm может быть заражена и украсть его данные/сломать ему систему.
Он просто ставит заранее надёжный wasm-рантайм а далее его ничего не волнует. Что там у вас, как там у вас. Он просто использует что угодно, где угодно, безопасно.
Правильно. Если вам нужен ГЦ в программе на С++ — вы пишите гц на С++. Это не вызывает никаких проблем и С++ ненужна нативная поддержка гц. Васм такой же С++.
Ну С++ — это внешний для .net мир. Я имел ввиду нативно в контексте .net. Ведь именно потому гц написан там на С++.
Именно поэтому .net и нужен С++ и нативнища, ведь байткод и vm там высокоуровневые. Wasm же — это совершенно другой случай.
llvm-ir тоже абстрактная виртуальная машина. Ключевым тут является не название, а смысл. Какого уровня эта машина и насколько полно она может обобщать эту аппаратную платформу.
WASM-runtime не занимается исполнением кода. Он компилируемый, как и ir. Да, этому коду(как и ir) требуется некий рантайм, но в чём проблема? Никто и не говорил, что это бесплатно.
К тому же, вы предлагали(насколько я понял) заменить это жава/дотнет байткодом, который ещё более медленный, на порядки более медленный. И для решения этой проблемы там есть специальный jit, который воспринимает этот байткод не как байткод, а как некий язык высокого уровня.
ir и, в меньшей степени, wasm — уже оптимизируются на уровне байткода и транслируются в бинарник почти 1в1.
Вы воспринимаете jvm/.net по своему, но всё это лишь обычные программы на том же C (C++/Rust/etc).
И этот слой нужен для изоляции, ведь ОС так же вносит множество оверхеда, как и все аппаратные средства изоляции в железе(процессоре). На самом деле да, в какой-то мере wasm и броузер заново изобретают ОС и работают хуже. Взять ту же изоляцию памяти. Но броузер не может стать ОС потому что уже есть ОС, есть вендорлок и никто никуда не уйдёт и никого никуда не пустят.
К тому же изоляция уровня ОС достаточна слаба и броузер(либо какая-то другая песочница) всё равно нужен и это так же будет накладывать множество ограничений. В том числе и по производительности.
jvm/.net — это такой же С++ код как и любой другой.
И в этом его смысл. Мы можем в броузере(в какой угодно другой песочнице) запустить любой C/C++/Rust/etc-софт, а это значит, что мы можем запустить и jvm и v8 и что угодно. Да, это будет медленнее, но.
Смысл в том, что это будет работать и работать хорошо. Причём максимально безопасно. Вы сможете загрузить любой нужный вам рантайм к пользователю в броузер и делать так что хотите. Какая вам разница с того, что нативная jvm будет работать быстрее? Считайте, что у пользователя просто процессор похуже.
Хоть жс и хорош, но он был ошибкой. Причины тому просты — никто не думал, что это выльется в полноценные приложения на жс. Броузер стал некой новой ОС и такой высокоуровневый язык очень плох как базовый язык платформы.
Правильно. Это попытка исправить проблему.
Быстрее — это просто следствие. Задача была «сделать максимально близкое к железу представление, которое может всё то — что может железо».
А это неважно. Тут фишка в том, что вы на си можете написать питон, а на питоне си нет.
Мы просто берём и пишем любую сборку мусора.
Нет. Ненужно ничего тащить. Вы, скорее всего, неправильно поняли. Когда говорят «тащить гц в васм» — это говорится именно про доступ из васма к js-объектам. Т.е. налаживания коммуникации между двумя отдельно запущенными в броузере vm. Некий такой ffi.
Но вам ничего не мешает собрать v8 под васм и запустить в броузере. Можете jvm собрать. Оно, конечно, так просто не соберётся т.к. использует очень много платформо-специфичных решений. И если их убрать/заменить на васм-специфичные, то никаких проблем не будет.
Нету там никакого нативно и в этом проблема. wasm — это именно llvm-ir, а не жава/дотнет-байткод. Об их фундаментальной разнице я говорил выше. Ни на каком жава/дотнет-байткоде вы jvm не напишите, а вот на ir/wasm вполне.
В этом фишка. Вы берёте базовый язык — это такой язык, который может «всё». А далее пишите что хотите, хоть jvm, хоть питон.
Да, это так и работает. Так и задумывалось. Люди правильно сделали.
Нет. Вы путаете слова и понятия. vm в разных контекста значит совершенно разное, ведь любая ОС — это по-сути vm.
Воспринимайте wasm как ОС + некую отдельную платформу(аппаратную). И уже далее на этой ПА платформе вы запускаете нативные программы. Такой нативной программой является mono. И люди её запустили. А ваш C#-код и его окружение — это уже ваши внутренние с .net/mono заморочки. wasm это не интересует, как и любую другую платформу(нативную или около того).
Намного эффективнее(он более низкоуровневый), нет привязки к рантайму(гц, операции над объектами и прочее).
llvm не совсем умеет отвязывать ir от архитектуры. База там действительно общая, но существует много всякой платформо-специфичной атрибутики. В целом ir почти во всём лучше wasm, но.
Основная фишка wasm — это именно изолированное(и эта изоляция намного ниже, нежели в ситуации с той же java) исполнение кода. Очевидно, что брать и генерировать asm — это не лучшая идея. Сам же llvm хоть и называется vm — vm не является.
Т.е. wasm — это такое безопасный ассемблер, который исполняется в вм. Он компилируемый, простой, крайне низкоуровневый, но он не позволяет читать/писать куда не надо. Это его основная задача и фундаментальное отличие от всего остального.
К тому же, данная фича в подавляющем большинстве случаев полезна, нежели вредна.
Что за глупости? Какое отношение все это имеет к ядру?
Нет. В линуксе(да и везде) маллок/free не занимаются памятью, а работают только с виртуальным адресным пространством.
Не всегда, очень даже не всегда.
К операционным системам это не имеет никакого отношения. Они лишь использует механизмы предоставленные железом.
Не виртуальная, как минимум не только виртуальная.
Память.
Нет. Менеджер памяти тут вообще не при делах и работает он, как я уже сказал, с адресным пространством. Т.е. он выделит просто диапазон и повесит на него права.
Отображением занимаются совершенно другие подсистемы. Причём занимаются только только созданием этих отображений — далее процессор всё делает сам.
Не для этого.
Это аппаратная фича.
Не для этого.
Не в некоторых, а вполне в конкретных. И всё там мотивировано.
Ничего из этого сделано не было. Да и советы достаточно глупые. Абстрактная нагрузка на процессор — ничего не показывает. Нужно использовать нормальный профайлер и собирать адекватный трейс, а уже далее смотреть.
Дак для чего же нужны эти большие страницы? Всё очень просто. Абсолютно неважно — сколько там страниц, пока они влезают в кэш. Есть специальный кэш — называется tlb. Когда происходит промах(т.е. отображения нету в кэше) — процессор вынужден пойти(автономно — ЭТО НЕ СИСТЕМНОЕ ВРЕМЯ) — найти и прочитать это отображение из памяти(если не нашел — тогда уже триггерится пейджфолт и уже тогда включается ядро).
Если отображение уже существует в памяти, то это на трупут почти не влияет. Процессор умеет в префетч и заранее подготавливает все отображения. Да, это съедает чуть-чуть трупута памяти(т.к. отображения читать из памяти нужно), но это крохи.
Но проблемы возникают именно на random access, когда никакой префетч не работает и трупут упирается в летенси(помноженной на блок) памяти. Поиск отображения — очень дорогая по летенси операция и от того задержки памяти возрастаю в разы.
Для предотвращения подобного поведения и нужны большие страницы. Их нужно меньше и в кэш влезают уже не мегабайты, а сотни, тысячи мегабайт памяти.
У этого подхода существует только одна проблема — это разряженная память. Дело в том, что по-умолчанию ядро попросту разрешает r/w в какой диапазон вадресспейса. Памяти в «памяти» выделяемой маллоком нет. Вообще(если это большие блоки(100к и больше по умолчанию, насколько я помню)).
Далее, когда мы начнём читать/писать в память — процессор не найдёт отображения и сгнерирует пейджфолт. Далее ядро пойдёт и в специальную таблицу(дерево) запишем отображение. Далее процессор всегда будет читать его или из памяти или из кэша(тлб).
Если мы будем читать/писать память байт за байтом, то первое чтение затригеррит пейджфолт и ядро свяжет первую(нулевую) страницу нашего отображения с реальной страницей(физической памяти). Для последующих 4096 байт(включая текущие чтение/запись) память будет. После — её не будет и опять будет пейджфол.
Тут уже можно понять — в чём проблема. Если мы выделим 2 мегабайта памяти и запишем половину — в ситуации с 4к страницами — выделится 1 мегабайт физической памяти. В ситуации с 2м страница — 2 мегабайта. Подобная ситуация сплошь и рядом в том же С++.
Тоже самое с разряженными массивами. Если у нас есть гигантский массив и мы сделаем туда 10 записей — мы затриггерем в среднем 10 пейджфолтом и свяжем 10 страница по 4к. Если тоже самое мы сделаем с 2м страницами — будет 10 по 2м.
Откуда может взяться высокий systime? Всё очень просто. Пейджфолты сами по себе дорогие, но разницы между типа страниц почти нет. Но.
Дело в том, что ядро при связывании страницы ОБЯЗАНО её забить нулями, т.е. пройтись мемсетом. Очевидно, что если мы в С++ запишем в строку 1гб + 1 байт, то ядро сделает мемсет на 1гб + 4к в случае с 4к страницами и 1гб + 2м в случае с 2м. И 1гб + 1гб в случае с 1гб страницами. Этим же обусловлено и возросшее потребление памяти.
И если в софте очень много разряженной памяти, то очевидно — ему повсеместные 2м+ страницы будут вредны, а в современном железе там 1гб и более страницы.
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
Чтобы дать приложениям возможность использовать фичу явно, если она им нужна.
Но почему-то их у вас нет.
Информация с perf абсолютно бесполезна. Нужно взять С++/раст-версию и запустить её на 1ккк вначале на never, а потом на always.
#!/usr/bin/env bash
set -e -o xtrace
cat /proc/cpuinfo
cat /proc/meminfo
sudo bash -c "echo never > /sys/kernel/mm/transparent_hugepage/enabled"
perf stat -dddd ./cpp-single $@
sudo bash -c "echo always > /sys/kernel/mm/transparent_hugepage/enabled"
perf stat -dddd ./cpp-single $@
Как-то так.
Версия гцц:
gcc (Gentoo 8.3.0 p1.0) 8.3.0
rust(100kk):
249511591
real 0m5,588s
user 0m5,545s
sys 0m0,043s
cpp(100kk):
249511591
real 0m5,188s
user 0m5,134s
sys 0m0,045s
rust(100kk):
249511591
real 0m5,613s
user 0m5,572s
sys 0m0,040s
cpp(100kk):
249511591
real 0m5,217s
user 0m5,173s
sys 0m0,043s
rust(100kk):
249511591
real 0m5,620s
user 0m5,580s
sys 0m0,040s
cpp(100kk):
249511591
real 0m5,192s
user 0m5,151s
sys 0m0,041s
rust(100kk):
249511591
real 0m5,596s
user 0m5,556s
sys 0m0,040s
cpp(100kk):
249511591
real 0m5,206s
user 0m5,161s
sys 0m0,044s
rust(1kkk):
2497558338
real 1m7,324s
user 1m6,325s
sys 0m0,995s
cpp(1kkk):
2497558338
real 1m2,467s
user 1m2,069s
sys 0m0,395s
rust(1kkk):
2497558338
real 1m7,135s
user 1m6,739s
sys 0m0,392s
cpp(1kkk):
2497558338
real 1m2,446s
user 1m2,019s
sys 0m0,423s
rust(1kkk):
2497558338
real 1m7,527s
user 1m7,133s
sys 0m0,390s
cpp(1kkk):
2497558338
real 1m2,494s
user 1m2,051s
sys 0m0,440s
rust(1kkk):
2497558338
real 1m7,133s
user 1m6,754s
sys 0m0,375s
cpp(1kkk):
2497558338
real 1m2,530s
user 1m2,139s
sys 0m0,383s
Да, именно поэтому вы не можете написать ваш код на голом хаскеле без либ, а я могу(написать аналогичный код на С/С++). Причина тому проста, и вы сами её где-то признали, что ваш массив и операции над ним — являются читами, а каноничным у вас является лишь интерфейс, который лишь создаёт видимость чистоты, ленивости и всего остального. Про соотношение массива и гц — я уже говорил.
Нет, речь шла не об этом. Речь шла о том, что вы вначале написали РАБОЧИЙ вариант на хаскеле(так, как привыкли), а далее началась борьба с его проблемами. И вы победили, в данном случае.
На С++ вы написали такой же РАБОЧИЙ вариант, но ни с чем вам воевать не пришлось. В этом разница.
Ну почему же. Вы по-сути сделали нам ретроспективу вашей победы. В любом случае, абсолютно неважно что там — массив/не массив. Всё остальное никуда не уходит. Я не знаю как видите вы, но не могу верить вам. И я не сомневаюсь в вашей честности, просто человеку свойственно обманываться в пользу своих убеждений.
Зачем вы врёте? Я не ссылался на питон — это вы на него ссылались. Я говорил о конкретных свойствах языков, которые пройдут этот тест — питон ему не соответствует. И вы его привели как контр-пример моим тезисам, но это не было контр-примером. Далее в своих рассуждениях на тему «почему примерно равная производительность равна»(в данном случае).
Да, я уже много примеров приводил. гц/операции над массивом. Т.е. массив по-умолчанию мутабельная структура и из неё нельзя сделать иммутабельную структуру(дёшево). Ваши операции мутабельные, массив по-сути является множеством значений(переменных), что так же не особо канонично. Вы сам в этом признавались, что если бы не «готовые функции» — никакой массив я бы использовать не мог.
Вот возьмите, пожалуйста, массив. Без использования готовой функции(которая взламывает вашу концепцию) инициализируйте его своим алгоритмом.
Нет, это ошибка. Ведь мы говорим об уровне языка и концепций. То, как именно пишите вы — мало на что влияет. Ведь я тоже могу писать на С++ безопасной и мой код будет безопасным, но концептуально это ничего не изменит, как и не изменит С++.
Это ограничения сверху. А ограничения сверху — это то, что матчасть волнует мало. Низ первичен — верх вторичен. Низ диктует условия — верх подчиняется. Низ прорывает абстракции верха, но низу насрать на абстракции верха.
Поэтому да, у вас нету постоянных адресов и это накладывает какие-то ограничения на вас. Но это никак не влияет на матчасть. Для матчасти ваши ограничения — лишь один из множеств возможных вариантов.
А причём тут «как вычислять»? «как вычислять» — это ваша локальная специфика, название уровня ваших абстракций. На уровне матчасти никаких «как вычислять нет», а есть код и данные, да и даже между ними разницы нет и код тоже данные.
Поэтому, что там навешано на объект — это неважно, неважно как оно называется на уровне абстракций.
Тут никаких противоречий нет.
Это именно семантика, в вашем случае. Ведь нужен был не просто массива, вы вкладывали в него иной смысл. И все вкладывают в него иной смысл. Возможно за вас кто-то уже вложил в используемый вами массив нужный смысл и именно этим вызвано ваше непонимание моих рассуждений.
Просто подумайте. У вас есть массив с value-типом. Подошел ли вам массив другой? Не с value-типом. Он бы соответствовал определённой вами семантики, но нет — он бы вам не подошел и вы бы искали другой. Потому что на этом уровне семантика чуть шире базовой.
Не знаю. Я не занимаюсь графиками. Если что-то нужно — использую упомянутый выше gnuplot, либо какой-нибудь онлайн-сервис. Но нужно это мне это крайне редко.
cat /proc/meminfo, perf stat -dddd Можете написать сюда выхлоп — разберёмся.
Ну дак у меня же всё меняется(при переключении), практика соотносится с теорией.
Это копейки на фоне всего остального. Там ниже есть мой код — там можно передать кол-во потоков. У меня на 30000потоков(на самом деле там создаётся далеко не 30к потоков, а гораздо больше) это добавляет 1-2секунды к 50 имеющимся. И это потоки ОС.
Ну я вижу с never такую же разницу, которую видите вы. Эта разница ничем, кроме влияния thp, не объясняется. По крайней мере ничем предложенным вами.
66,468976022 seconds time elapsed - go mt
64,819841304 seconds time elapsed - go
64,380398227 seconds time elapsed - go v3
64,001787006 seconds time elapsed - hs
62,100266396 seconds time elapsed - c++ + gcc
47,760004537 seconds time elapsed - c++ mt + gcc
51,243138067 seconds time elapsed - c++ mt + clang//я проверил множество раз
80 sec - nodejs(32 бита числа)
67,263350647 seconds time elapsed - rust(rustc 1.33.0)
50,179576857 seconds time elapsed - rust mt
47,933258835 seconds time elapsed - rust mt + taskset
Там дело не совсем в процессоре.Я уже писал про transparent huge pages.
У вас «многопоточность» не код ускорят, а tlb-промахи. Т.е. если отображения нету в кэше(tlb) — процессор идёт искать отображение в памяти. В случае с одним потоком — ядро(процессор) почти всё время(половину) занимается поиском. В многопотоке этот поиск размазывается по ядрам.
75,197948177 seconds time elapsed// - ваш
124,388672662 seconds time elapsed// - который "однопоточный"
66,385085639 seconds time elapsed// ваш на всех потоках.
58,259779411 seconds time elapsed// ваш, все ядра и thp
Нет, я бы это увидел. Ваша пилит на половине ядер, другая на всех, а ещё одна на ядре + гипетред(как и говорил автор).
go version go1.12.1 linux/amd64
go run, но я не думаю, что это на что-то влияет. Там гцц собирает С++-код (миллионы строк) за 0.4sec.
Там ненужно много ядер — оно уже двух перестаёт расти, а на 4х начинает деградировать.
Нет, с чего вдруг? Алгоритм говном — для него всё плохо.
Нет. Дефолтная десктопная/e3 платформа штеуда утилизирует практически 70-80 трупута памяти. Поэтому никакие наивные представления про ускорение — здесь не прокатывают. Да, код подобного уровня никогда не снимет даже 10-20% трупута памяти, поэтому и существует какие-то наивные оценки на тему.
Но тут есть одна проблема. Из-за того, что алгоритм говно — получается так, что он съедая 10-20% трупута — съедает почти весь.
Причина заключается в следующим: кэш не умеет читать память байтами, 4 байта, 8 байтами, даже 32 не умеет — только кешлайнами, а это 64 байта.
Поэтому, при RA в память в подобном кейсе на каждое чтение int64_t на самом деле читает 64 байта, то же самое и с записью. Именно поэтому и наблюдает такое поведение. Код говно, алгоритм говно, а каналы до памяти все сожраны.
К тому же. не стоит забывать, что атомики сами по себе имеют большой оверхед и сравнивать надо не с дефолтным вариантом, а с запуском на одном треде(второй аргумент там — кол-во тредов). Там, скорее всего, будет секунд 100(если не больше) — я не проверял.
Побенчил ещё то, что мог запустить:
66,468976022 seconds time elapsed - go mt
64,819841304 seconds time elapsed - go
64,380398227 seconds time elapsed - go v3
64,001787006 seconds time elapsed - hs
62,100266396 seconds time elapsed - c++ + gcc
47,760004537 seconds time elapsed - c++ mt + gcc
51,243138067 seconds time elapsed - c++ mt + clang
80 sec - nodejs(32 бита числа)