В последнее время мне встречалось немало статей, в которых не самым удачным для меня образом продвигается свежий сборщик мусора в Go. Некоторые из статей написали разработчики самого языка, и их утверждения намекали на радикальный прорыв в технологии сборки мусора.
Вот первичный анонс о внедрении нового сборщика, датированный августом 2015-го:
В Go создаётся сборщик мусора (GC) не только для 2015 года, но и для 2025-го, и ещё дальше… Сборщик в Go 1.5 возвещает о наступлении будущего, в котором паузы на сборку больше не являются барьером для перехода на безопасный язык. Это будущее, в котором приложения без труда масштабируются вместе с оборудованием, и по мере роста мощности оборудования сборщик мусора больше не является сдерживающим фактором при создании более качественного, масштабируемого ПО. Go — хороший язык для использования как минимум в ближайший десяток лет.
Создатели утверждают, что они не просто решили проблему пауз на сборку мусора, а пошли куда дальше:
Одним из высокоуровневых способов решения проблем с производительностью является добавление GC-настроек (knobs), по одной на каждую проблему. Программист может менять их, подбирая наилучшую комбинацию для своего приложения. Недостатком этого подхода является то, что при внедрении каждый год одной-двух новых настроек через десять лет придётся законодательно регулировать труд людей, которые будут менять эти настройки. Go не пошёл по этому пути. Вместо кучи настроек мы оставили одну и назвали её GOGC.
Более того, освободившись от бремени поддержки десятков настроек, разработчики могут сосредоточиться на улучшении runtime’а приложения.
Не сомневаюсь, что многие пользователи Go были просто счастливы получить новый подход к runtime’у в Go. Но у меня есть претензии к этим заявлениям: они выглядят как недостоверный маркетинговый булшит. А поскольку они раз за разом воспроизводятся в Сети, пришло время подробно с ними разобраться.
Реальность такова, что в сборщике мусора в Go не реализовано никаких новых идей или результатов исследований. Как признаются сами авторы, это просто сборщик с параллельной пометкой и очисткой, основанный на идеях 1970-х. Это примечательно лишь потому, что сборщик был разработан для оптимизации продолжительности пауз за счёт всех остальных важных характеристик сборщика. В технических и маркетинговых материалах Go не упоминается обо всех этих побочных эффектах, поэтому многие программисты не подозревают об их существовании. В результате создаётся впечатление, что конкурентные языки — плохо спроектированный шлак. И авторы Go лишь подогревают эти мысли:
Для создания сборщика на следующее десятилетие мы обратились к алгоритмам из десятилетий прошлых. В нашем сборщике реализован трёхцветный (tri-color) алгоритм параллельной пометки и очистки, предложенный Dijkstra в 1978 году. Это намеренное отличие от большинства современных сборщиков «корпоративного» класса, которое, как мы считаем, лучше всего соответствует особенностям современного оборудования и требованиям по уровню задержки в современном ПО.
Читаешь всё это, и возникает мысль, что за последние 40 лет в сфере «корпоративных» сборщиков мусора ничего лучше предложено не было.
Введение в теорию сборки мусора
При разработке алгоритма сборки мусора нужно учитывать ряд факторов:
- Пропускная способность программы: насколько алгоритм замедлит скорость работы программы? Иногда это выражается в процентах: отношение времени процессора, потраченного на сборку, ко времени, потраченному на полезную работу.
- Пропускная способность сборщика: сколько мусора может вычистить сборщик за определённое время работы процессора?
- Избыточность кучи: сколько памяти сверх теоретического минимума потребуется сборщику? Если в ходе сборки он размещает в памяти временные структуры, не приведёт ли это к резкому росту потребления памяти программой?
- Продолжительность пауз: на какой срок сборщик останавливает работу программы?
- Частота пауз: как часто сборщик останавливает работу программы?
- Распределение пауз: большинство пауз очень короткие и лишь несколько очень длинные? Или вы предпочитаете делать длительность пауз более равномерной?
- Производительность размещения в памяти: размещение в новой памяти выполняется быстро, медленно или непредсказуемо?
- Уплотнение: выдаёт ли сборщик сообщение об отсутствии памяти (out-of-memory, OOM), даже если места для удовлетворения его запроса достаточно, но оно рассеяно по куче в виде маленьких чанков? Если не выдаёт, то программа может начать замедляться и в конце концов просто встать, даже если памяти для продолжения работы достаточно.
- Многопоточность: насколько эффективно сборщик использует многоядерные машины?
- Масштабирование: насколько эффективно сборщик работает по мере увеличения размера куч?
- Настройка: насколько сложно настроить сборщик прямо из коробки, а также для достижения оптимальной производительности?
- Продолжительность прогрева: является ли алгоритм самонастраивающимся на основе измерений собственного поведения? Если да, то как долго он выходит в оптимальный режим работы?
- Освобождение страницы: алгоритм возвращает операционной системе неиспользуемую память? Если да, то когда?
- Портируемость: работает ли сборщик на процессорных архитектурах, предоставляющих более слабые гарантии консистентности памяти по сравнению с x86?
- Совместимость: с какими языками и компиляторами работает сборщик? Можно ли запустить его с языком, который создавался без учёта использования сборщиков, например С++? Нужно ли модифицировать компилятор? Если да, потребуется перекомпилировать всю программу и зависимости при изменении алгоритма сборщика?
Как видите, при разработке сборщика нужно учитывать много разных факторов, и некоторые из них влияют на архитектуру более широкой экосистемы, связанной с вашей платформой. Причём я не уверен, что перечислил все факторы.
Из-за сложности пространства проектных параметров сборка мусора представляет собой подобласть информатики, богато освещённую в исследовательских работах. Новые алгоритмы предлагаются и внедряются регулярно, как в академической, так и в коммерческой среде. Но, к сожалению, никто ещё не создал алгоритма, подходящего для всех случаев жизни.
Кругом сплошные компромиссы
Разберёмся с этим подробнее.
Первые алгоритмы сборки мусора разработали для однопроцессорных компьютеров и программ с маленькими кучами. Ресурсы процессора и памяти были дороги, а пользователи — нетребовательны, так что к паузам в работе программ относились лояльно. Создававшиеся в те времена алгоритмы старались поменьше задействовать процессор и минимизировать избыточное потребление памяти в куче. Это означало, что сборщик ничего не делал до тех пор, пока программа могла размещать в памяти данные. Затем она вставала на паузу до полного выполнения пометки и очистки кучи, чтобы как можно скорее освободить часть памяти.
У старых сборщиков есть преимущества: они просты; не замедляют программу, если не выполняют свою работу; не приводят к избыточному потреблению памяти. Консервативные сборщики, например Boehm GC, даже не требуют вносить изменения в компилятор или язык программирования! Это делает их подходящими для настольных приложений (обычно их кучи маленького размера), в том числе для видеоигр категории ААА; в них большая часть памяти занята файлами с данными, которые не нужно сканировать.
Алгоритмы, для которых характерны паузы полной остановки (Stop-the-world, STW) для выполнения пометки и очистки, чаще всего изучают на курсах по информатике. Иногда на собеседованиях я прошу кандидатов немного рассказать о сборке мусора. И чаще всего они представляют сборщик как чёрную коробку, внутри которой неизвестно что происходит, либо считают, что в нём используется очень старая технология.
Проблема в том, что такие простые паузы на пометку и очистку крайне плохо масштабируются. Если добавить ядра и увеличить соотношения объёмов куч и размещений в памяти, то алгоритм перестаёт хорошо работать. Но иногда, когда применяются маленькие кучи, даже простые алгоритмы вполне сносно выполняют свою задачу! В подобных ситуациях вы можете воспользоваться подобным сборщиком и минимизировать избыточность потребления памяти.
Другая крайность — использование куч размером в сотни гигабайтов на машинах с десятками ядер. Например, на серверах, обслуживающих биржевые транзакции или поисковые запросы. В подобных ситуациях нужно сделать паузы как можно короче. И тогда предпочтительнее могут быть алгоритмы, в целом замедляющие работу программ за счёт фонового сбора мусора, но зато с очень короткими паузами.
На мощных системах вы также можете выполнять большие пакетные задания. Для них важны не паузы, а только общее время выполнения. В таких случаях лучше использовать алгоритм, который максимизирует пропускную способность (throughput), то есть отношение выполненной полезной работы ко времени, потраченному на сборку мусора.
Увы, нет ни одного алгоритма, полностью идеального. Также runtime ни одного языка не может определить, является ли ваша программа пакетным заданием или интерактивной программой, чувствительной к задержкам. Именно это, а не глупость разработчиков runtime’а, привело к появлению «настроек сборщика мусора». Это следствие фундаментальных ограничений информатики.
Гипотеза поколений (generational hypothesis)
В 1984 году было подмечено, что большинство размещений в памяти «умирают молодыми», то есть становятся мусором через очень короткое время после размещения. Это наблюдение, названное гипотезой поколений, — одно из сильнейших эмпирических наблюдений в сфере разработки языков программирования. Гипотеза вот уже несколько десятилетий подтверждается для самых разных языков: функциональных, императивных, не имеющих и имеющих типы данных.
Гипотеза поколений принесла пользу в том смысле, что алгоритмы сборки мусора стали использовать её плюсы. Так появились сборщики на основе поколений (generational collectors), которые имели ряд преимуществ по сравнению со старыми алгоритмами «остановить — пометить — очистить»:
- Пропускная способность сборщика: они могут собирать гораздо больше мусора и гораздо быстрее.
- Производительность размещения в памяти: при размещении новой памяти больше не нужно искать свободные слоты в куче. Так что размещение, по сути, стало бесплатным.
- Пропускная способность программы: фрагменты памяти стали аккуратно размещаться рядом друг с другом, что сильно улучшило эффективность использования кеша. Сборщики на основе поколений требуют, чтобы программа выполняла определённую работу по мере выполнения, но на практике это перевешивается преимуществами от изменений в работе кеша.
- Продолжительность пауз: большинство пауз (но не все) становятся короче.
Но у таких сборщиков есть и недостатки:
- Совместимость: алгоритмы перемещают данные в памяти и делают дополнительные манипуляции, когда программа в некоторых случаях пишет в указатель. Это означает, что сборщик должен быть тесно интегрирован с компилятором. Для С++ не существует сборщиков на основе поколений.
- Избыточность кучи: такие сборщики копируют фрагменты памяти туда и обратно между разными «пространствами». Поскольку должно быть достаточно места для копирования, возникает определённая избыточность размера кучи. К тому же такие сборщики требуют поддержки различных карт указателей (запоминаемые множества — remembered sets), что ещё больше повышает избыточность.
- Распределение пауз: хотя многие паузы очень коротки, иногда всё же требуются полные остановки на пометку и очистку в рамках всей кучи.
- Настройка: сборщики на основе поколений ввели понятие «молодое поколение», или «райское пространство» (eden space), и от его размера сильно зависит производительность программы.
- Продолжительность прогрева: в ответ на проблему настройки некоторые сборщики динамически адаптируют размер молодого поколения на основе данных о поведении выполняемой программы. Но теперь паузы стали зависеть и от продолжительности работы программы. Обычно это важно только для результатов бенчмарков.
Тем не менее выигрыш от использования сборщиков на основе поколений так велик, что сегодня этот тип абсолютно доминирует. Если вы готовы смириться с недостатками, то вам наверняка понравятся такие сборщики. Эти алгоритмы можно расширять всевозможными функциями, типичные современные сборщики могут быть в одном лице многопоточными, параллельными, уплотняющими (compacting) и использующими поколения.
Параллельный сборщик в Go
Go — довольно обыкновенный императивный язык с типами значений. Пожалуй, его шаблоны доступа к памяти можно сравнить с С#, в котором используется гипотеза поколений, следовательно, применяется сборщик .NET.
По факту программы на Go обычно требуют наличия обработчиков запросов/откликов вроде HTTP-серверов. То есть они демонстрируют поведение, сильно завязанное на поколениях. Создатели Go думают, как это можно использовать в будущем с помощью таких вещей, как «сборщик, ориентирующийся на запросы» (request oriented collector). Как уже заметили, это просто переименованный сборщик на основе поколений с настроенной политикой срока владения.
Можно эмулировать такой сборщик в других runtime’ах для обработчиков запросов/откликов. Для этого нужно удостовериться, что молодое поколение достаточно велико, чтобы в него поместился весь мусор, генерируемый при обработке запроса.
Но несмотря на это, используемый сегодня в Go сборщик — это не сборщик на основе поколений. Он просто выполняет в фоне старую добрую процедуру пометки с очисткой.
У такого подхода есть одно преимущество: можно получить очень-очень короткие паузы. Но все остальные параметры ухудшатся. Например:
- Пропускная способность сборщика: время, необходимое для очистки кучи от мусора, увеличивается с размером кучи. Чем больше памяти использует ваша программа, тем медленнее эта память освобождается и тем больше времени компьютер тратит на сборку по сравнению с полезной работой. Вы избежите всего этого, только если программа вообще не распараллеливает своё выполнение, но вы можете без ограничений продолжать использовать ядра для сборки мусора.
- Уплотнение: поскольку оно не выполняется, программа может в результате фрагментировать всю кучу. Об этом мы ещё поговорим ниже. Также вы не получите преимуществ от аккуратного использования кеша.
- Пропускная способность программы: в каждом цикле сборщик должен делать много работы. На это уходит больше времени процессора, которое могло быть отдано самой программе, а она из-за этого работает медленнее.
- Распределение пауз: сборщик, выполняющийся одновременно с программой, может привести к тому, что в мире Java называется сбоем режима совместного выполнения (concurrent mode failure): программа генерирует мусор быстрее, чем треды сборщика успевают его чистить. В этом случае runtime’у приходится полностью останавливать программу и ждать завершения цикла очистки. Так что когда авторы Go утверждают, что паузы очень короткие, то это относится только к случаям, когда сборщику достаточно времени процессора, чтобы не отставать от программы. Кроме того, компилятору Go не хватает возможностей, чтобы надёжно и быстро ставить треды на паузу. То есть длительность пауз сильно зависит от выполняемого вами кода (например, base64-декодирование крупного блоба в одиночной горутине может привести к увеличению пауз).
- Избыточность кучи: учитывая медленность сборки мусора в куче с помощью пометки и очистки, вам нужно много места в запасе, чтобы не столкнуться со сбоем режима совместного выполнения. По умолчанию в Go предусмотрена стопроцентная избыточность… то есть он удваивает объём памяти, необходимой вашей программе.
Вот отрывок из одного поста, в котором рассказывается о вышеописанных недостатках:
Сервис 1 размещает больше памяти, чем Сервис 2, поэтому паузы полной остановки у него длиннее. Однако у обоих сервисов абсолютная продолжительность пауз остановки уменьшается на порядок. После включения на обоих сервисах мы наблюдали ~20%-й рост потребления сборщиком времени процессора.
В данном случае продолжительность пауз в Go снизилась на порядок, но за счёт замедления работы сборщика. Можно ли это счесть оправданным компромиссом или длительность пауз и так уже была достаточно низкой? Автор не сказал.
Однако наступает момент, когда больше не имеет смысла наращивать возможности железа для сокращения пауз. Если паузы на сервере снизятся с 10 до 1 мс, заметят ли это пользователи? А если для такого снижения вам потребуется увеличить аппаратные ресурсы вдвое?
Go оптимизирует длительность пауз за счёт пропускной способности, причём настолько, что кажется, будто он хочет на порядок замедлить работу вашей программы, лишь бы сделать паузы чуточку меньше.
Сравнение с Java
Виртуальная машина Java HotSpot имеет несколько алгоритмов сборки мусора. Вы можете выбирать их через командную строку. Ни один из них не старается так сильно снизить продолжительность пауз, как Go, потому что они пытаются поддерживать баланс. Чтобы прочувствовать влияние компромиссов, можно сравнить алгоритмы друг с другом, переключаясь между разными сборщиками. Как? С помощью простого перезапуска программы, потому что компилирование выполняется по мере её исполнения, так что разные барьеры, необходимые для разных алгоритмов, могут по мере необходимости компилироваться и оптимизироваться в коде.
На современных компьютерах по умолчанию используется сборщик на основе поколений. Он создан для пакетных задач и изначально не обращает внимания на длительность паузы (её можно задавать в командной строке). Из-за возможности выбирать сборщик по умолчанию многие считают, что в Java паршивый сборщик мусора: из коробки Java пытается сделать так, чтобы ваше приложение работало как можно быстрее, с наименьшей избыточностью памяти, а на паузы наплевать.
Если вам нужно уменьшить продолжительность пауз, то можете переключиться на сборщик с параллельной пометкой и очисткой (concurrent mark/sweep collector, CMS). Это самое близкое к тому, что используется в Go. Но это алгоритм на основе поколений, поэтому паузы у него длиннее, чем в Go: молодое поколение уплотняется во время пауз, потому что выполняется перемещение объектов. В CMS есть два типа пауз: покороче, около 2—5 мс, и подлиннее, около 20 мс. CMS работает адаптивно: поскольку он выполняется одновременно (concurrent), то должен предполагать, когда ему запуститься (как и в Go). В то время как Go попросит вас сконфигурировать избыточность кучи, CMS самостоятельно адаптируется в ходе runtime, стараясь избежать сбоев режима одновременного выполнения. Поскольку большая часть кучи обрабатывается с помощью обычной пометки и очистки, то можно столкнуться с проблемами и тормозами из-за фрагментации кучи.
Самое свежее поколение сборщика в Java называется G1 (от garbage first). По умолчанию он работает начиная с Java 9. Авторы постарались сделать его как можно более универсальным. По большей части он выполняется одновременно, основан на поколениях (generational) и уплотняет всю кучу. Во многом самонастраиваемый. Но поскольку он не знает, чего вы хотите (как и все сборщики мусора), то позволяет регулировать компромиссы: просто укажите максимальный объём памяти, который вы ему выделяете, и размер пауз в миллисекундах, а всё остальное алгоритм подгонит самостоятельно, чтобы соблюсти ваши требования. По умолчанию длительность пауз около 100 мс, так что, если вы не уменьшите их самостоятельно, не ждите, что это сделает алгоритм: G1 отдаст предпочтение скорости работы приложения.
Паузы не совсем консистентны: большинство очень коротки (менее 1 мс), но есть и несколько длинных (более 50 мс), связанных с уплотнением кучи. G1 прекрасно масштабируется. Известны отзывы людей, которые применяли его на кучах терабайтного размера. Также у G1 есть ряд приятных возможностей вроде дедупликации строк в куче.
Наконец, был разработан ещё один новый алгоритм под названием Shenandoah. Он внесён в OpenJDK, но в Java 9 не появится, пока вы не станете использовать специальные билды Java из Red Hat (спонсора проекта). Алгоритм разработан с целью минимизации продолжительности пауз, невзирая на размер кучи, которая в то же время уплотняется. К недостаткам относятся большая избыточность кучи и ряд барьеров: для перемещения объектов во время выполнения приложения необходимо одновременно считывать указатель и взаимодействовать со сборщиком мусора. В этом смысле алгоритм аналогичен «безостановочному» сборщику из Azul.
Заключение
Цель этой статьи — не в том, чтобы убедить вас использовать другие языки или инструменты. Сборка мусора — это трудная, действительно трудная проблема, которая десятилетиями изучается армией учёных и программистов. Поэтому относитесь с подозрением к прорывным решениям, которые никто не заметил. Вероятнее всего, это просто странные или необычные замаскированные компромиссы, избегаемые другими по причинам, которые станут ясны позднее.
А если хотите минимизировать продолжительность пауз за счёт всех остальных параметров любой ценой, то обратитесь к сборщику мусора из Go.
Комментарии (188)
googol
03.01.2017 22:14+11Лучший сборщик мусора это тот который не нужен. Мне нравится как в Расте подошли к этому вопросу. Язык располагает к тому чтобы структуры выделялись на стеке, а динамическая память отслеживается через reference counts.
senia
03.01.2017 23:20Это, конечно, круто (сам фанат Rust), но как с фрагментацией памяти бороться будем?
lorc
03.01.2017 23:30Ммм… Slab allocator? Можно автоматически создавать кеши для часто аллоцируемых\деаллоцируемых объектов. Это не решит проблему фрагментации на 100%, но позволит поддерживать её на приемлимом уровне.
TargetSan
03.01.2017 23:32Насколько я помню, применяется метод нескольких пулов страниц по размеру. Допустим, менеджер памяти откусывает страницы по 4 килобайта. Мы создаём несколько пулов объектов — 32, 64, 128, 256, 512, 1024, 2048 байт. Всё что больше — выделяется непрерывными кусками без особых заморочек. Как результат такой "сегрегации" — проще найти дырку под новый объект на месте удалённого.
senia
03.01.2017 23:55Пулы должны иметь предустановленный размер (или выделяться кусками), плюс под объект выделяется больше памяти, чем необходимо. С учетом этого не окажется ли перерасход памяти сравнимым с GC?
Хранение списка свободных страниц и его поддержание требует ресурсов.
Последовательное выделение в таком пуле приводит к не последовательному выделению памяти (теряется colocation), что ухудшает попадание данных в кэш — всегда ли этим можно пренебречь?
Выделение на объект больше памяти, чем необходимо, из-за пулинга тоже отрицательно влияет на попадание в кэш.
Я не готов утверждать, что все это в каких-то случаях приведет к выигрышу GC, но может когда-то соберусь написать бенчмарки.TargetSan
04.01.2017 00:11Мне кажется, вы сравниваете с конкретно Generational GC — у которого тоже есть свои проблемы. Но не будем, тут действительно нужно писать бенчмарки — и притом не тривиальную сумму миллиона чисел в массиве.
Лично для меня преимущество у не-GC языков в большей управляемости и, что важнее, едином в меру удобном механизме работы с любыми ресурсами — не только памятью. В управляемой среде нетривиальный сценарий работы с тем же файлом, критической секцией, COM-объектом или кустом реестра превращается в мороку. В С++ и Расте компилятор хоть деструкторы сгенерит.
akzhan
04.01.2017 11:58В том же C# есть
using(var resource = new MyUnmanagedResource) { # do something with resource } # auto dispose resource
TargetSan
04.01.2017 12:14+2Я в курсе. Как по мне, это костыль.
Два недостатка:
- Менеджмент ручками. Ответственность за удаление перекладывается с автора кода ресурса на пользователя. Забыли юзинг — ресурс освободится неизвестно когда, если вообще освободится.
- Как только ресурс живёт дольше области видимости одной функции — using блок становится бесполезен
lany
04.01.2017 14:43+4- Вы ещё скажите что в С++ на пользователе ресурса нет никакой ответственности и он не может забыть использовать смарт-поинтер и забыть написать delete. Плюс адекватные IDE вам частенько подсветят, если вы чего забыли.
- При правильном дизайне это решается тривиально. Если ресурс надо закрывать при окончании использования владельца (условно при его деструкции), то владелец объявляется новым ресурсом (в джава интерфейс
AutoCloseable
) и уже его пользователи используют using/twr. Если жизненный цикл более мутный, на помощь придут пулы ресурсов. Собственно у меня начиная с седьмой джавы не было никаких проблем с ресурсами в новом коде. До этого действительно бойлерплейта было много и можно было неаккуратно закрыть всё в исключительных ситуациях, что могло приводить к утечкам. Сейчас об этом особо думать не надо.
senia
04.01.2017 21:361. Да, C++ далеко не идеален. В Rust же, благодаря идеологии владения, на пользователе действительно нет ответственности (в разумных пределах, естественно).
2. При совместном использовании ресурса из нескольких потоков AutoCloseable не поможет.
leventov
11.01.2017 00:55+1Этот подход работает почти всегда, но не всегда. Например если между "передачей" или созданием ресурса и завершением конструктора "приемщика" происходит исключение, например какой-нибудь NPE или ISE, ресурс утечет, если не добавить try-catch Throwable. Вроде бы NPE/ISE быть "не должно", но если не добавить этот боилерплейт, сервер может быть дестабилизирован: вместо "иногда на определенных запросах валится с исключением" получаем то же самое + "постепенно течет и со временем валится полностью", по исчерпанию direct memory или какого-нибудь пула коннектов, например.
TargetSan
03.01.2017 23:34+2Это скорее результат старого проверенного подхода "всё есть ресурс в т.ч. память". Кстати, это одна из проблем языков со сборкой мусора — работа с временем жизни неуправляемых ресурсов превращается в редкий геморрой вне самых простых сценариев.
lany
04.01.2017 14:46+1Не вижу, чтобы в джаве с этим было больше геморроя, чем в С++. Вы много управляли ресурсами в языках со сборкой мусора? Приведите пример, когда в джаве/шарпе с ресурсом геморрой, а в плюсах аналогичная проблема решается легко и просто.
senia
04.01.2017 21:39RAAI. Постоянный using и try-with-resources не только засоряют код, но их еще и легко забыть. А использование из нескольких потоков невозможно. В Rust же RAAI + Arc + Lock и ресурс можно даже между потоками шарить.
leventov
11.01.2017 00:59+1В Java гораздо сложнее управлять связками управляемой и неуправляемой памяти, в C++ это одно и то же
chabapok
05.01.2017 03:38Так и в java есть эскейп-анализ (хранение полей объекта в стеке)
У раст-подхода есть свои недостатки и достоинства. Например, большое кол-во маложивущих объектов в куче вызовет, скорей всего, приличные расходы на аллокацию и gc. А если это делается в несколько потоков, то можно нарваться на false sharing (это предположение), что просадит производительность раз 100..1000. В java первоначальная аллокация происходит в своей для каждого потока области, И возможно даже, что адреса областей выбраны так, чтобы физическая память мапилась в логическую не образовывая списки, и плюс всякие другие фишки.
А еще в раст не очень понятно, как работать с циклическими ссылками.
Раст хорош по-своему, но не идеален. Надо это понимать.DarkEld3r
10.01.2017 19:09Например, большое кол-во маложивущих объектов в куче вызовет, скорей всего, приличные расходы на аллокацию и gc.
Такие объекты лучше будет прямо на стеке и создать, если есть возможность.
А еще в раст не очень понятно, как работать с циклическими ссылками.
Понятно, но не особо удобно. (:
khim
10.01.2017 21:20
Так это достоинство, а не недостаток! Типичная файловая система в принципен с циклическими ссылками работать не может — и ничего, это не мешает в ней хранить всё, что придумало человечество, почему-то.А еще в раст не очень понятно, как работать с циклическими ссылками.
Понятно, но не особо удобно. (:
ZyXI
10.01.2017 21:34Циклические символические ссылки никуда не делись. Полагаю, это должно быть что?то вроде аналога слабых ссылок? В любом случае, циклические ссылки есть, с ними «работают», но отваливаются они по количеству переходов, а не потому, что кто?то нашёл цикл.
khim
10.01.2017 21:48Полагаю, это должно быть что?то вроде аналога слабых ссылок?
Они самые. И поддерживаются они далеко не на всех фс.
zekohina
03.01.2017 22:21-11Вкратце суть статьи: GC в go не тормозит, но программы жрут больше памяти. В java GC тормозит, но программы жрут меньше памяти.
P.S. Но мы то знаем кто из них на самом деле жрет больше памяти :)Tsimur_S
03.01.2017 23:20+1мне кажется главный поинт статьи: java позволяет выбрать тот алгоритм сборки мусора который вам нужен, а в Go он вшит намертво из непонятных побуждений (скорее всего заточен под сценарий использований в Google и для них продолжительность и равномерность пауз важнее чистой производительности, которую можно увеличить доп. железом ).
creker
04.01.2017 01:47+11Побуждения предельны ясны и озвучены были с самого рождения языка. Это язык для backend сетевых приложений, а значит задержки должны быть минимальны. Можно тратить память, долго ее высвобождать, но не тормозить ответы на запросы. Ведь пользователю плевать, какой там крутой сборщик мусора. На его конце он видит только скорость, с которой сервер отдает ответ.
googol
04.01.2017 23:14+2Достану-ка я балалайку. В свою бытность, в годах 2008-2010 я работал как java frontend engineer в гугле над проектом gmail. Физической памяти на серверных машинах в то время было 32Gb если помню точно. Каждый гмейловский task большой и долгоживущий. Как вы могли догадаться съедали эти процессы всю доступную память. Мелких объектов много, а GC был не ахти какой оптимизированный. И переодически gmail процесс останавливался чтобы GC могла почистить память. Длинные остановки была нашей головной болью. Мы логировали информацию об остановках GC для того чтобы исследовать поведение поподробнее. Так вот самая длинная остановка GC на моей памяти была 95 секунд! Девяносто пять секунд, Карл.
chabapok
05.01.2017 03:55Интересно. А память увеличивать пробовали, или это был предел?
Грубо говоря, если увеличение памяти сокращает продолжительность паузы, то значит, размер памяти не соответствовал масштабу задачи. А если не сокращает, а просто делает их реже — значит либо плохо написана программа, либо плохой gc. Конечно, это грубо.
Вообще, full gc это такой костыль, который призван кое-как компенсировать негативный эффект от повышенного пожирания памяти. В с++ нет никакого full gc, Если памяти для плюсовой проги не хватит — то прога скорей всего просто упадет и никому в голову не приходит «оптимизировать дырки в хипе», просто говорят, мол, это большая задача — и добавляют памяти. Но для java такой подход почему-то неприемлят — и начинают ругать gc.gorodnev
05.01.2017 14:25А в С++ есть сборка мусора?
chabapok
05.01.2017 15:16А где-то утверждалось, что есть?
Вы к пустяку придрались, к словам. Речь была о том, что есть плюсовой подход: не латаем дырки в куче, а говорим сразу — не шмогла, дай памяти. И есть java подход: латаем дырки и жалуемся, что тормоза.
p.s. вообще есть boehm gc напрмиер. Но к делу это не относится.
Valle
04.01.2017 05:43+3Гм. Мне вот как-то понадобилось в яве задержки не более двух миллисекунд на мусор на время нескольких минут. Я неделю долбался, но единственный вариант оказался тем, что сборщик мусора пришлось вообще отключить путем установки эдема в ~8 гигабайт, хотя потребление общее памяти было мегабайт 10. Если в GO сборщик мусора дает миллисукундные задержки из коробки то пусть хоть обпишутся, но го делает яву в этом отношении просто без вариантов.
rustler2000
04.01.2017 23:00+6Ну вот не выдержал — https://twitter.com/brianhatfield/status/692778741567721473 — это на 18гиг хипе )))
AllexIn
03.01.2017 23:21Вы в курсе, что в Java — есть выбор из множества GC? Какой конкретно тормозит и жрет больше памяти?
Не отвечайте. Вопрос не корректный. Т.к. всё зависит не только от GC, но еще и от задачи.
На разных задачах разные GC будут показывать разные результаты. Об этом и статья.Valle
04.01.2017 05:46+1Можно пожалуйста настройки Oracle JDK GC в студию при которых гарантируется задержки на GC не более 1 миллисекунды? (Понятно, что при разумной скорости генерации мусора)
thousandsofthem
03.01.2017 23:32Если так то разумный выбор т к го генерирует меньше мусора в памяти и использует довольно компактные типы данных
pda0
04.01.2017 00:28+8Мне тема со сборкой мусора напоминает алхимию и поиски философского камня. До того, как языки со встроенной сборкой мусора стали популярны, задача корректного освобождения памяти требовала определённых усилий. Надо было не забыть освободить память и сделать это правильно (delete vs. delete[] vs. free). С этим можно было справиться при помощи инструментов, типа valgrid и статических анализаторов. Т.е. требовало усилий и дисциплины от программиста.
Казалось бы, сборка мусора должна решить эту проблему. Но идут годы, а мы читаем одни и те же статьи, про то, как разработчики языков улучшают сборщики мусора и вот-вот сделают всем хорошо (хотя их работа — мега сложная). А команды разработчиков пишут уже о двух проблемах. О том, как они решают проблемы с несвоевременным освобождением внешних ресурсов (финализаторы порой вызываются когда им вздумается) и про то, как они героически тюнили сборщик мусора под их высоконагруженный проект. (И, конечно, в конце-концов победили. По крайней мере, пока.)
Вам не кажется, что для сложных проектов сборка мусора создаёт большем проблем, чем решает?gagoman
04.01.2017 00:37+4Разные проекты, требования, подходы и сроки. Где NASA может тратить 1000$ на строчку кода, обычно тратится в разы меньше. Этим и вызван взрывной рост интернета. Если бы все писали, считая delete vs delete[] vs free, то непонятно где бы мы были. Все как всегда — «каждой задача по технологии»
pda0
04.01.2017 00:44Только где дешевле, а где дороже. Статьи типа «у нас возникли проблемы в продакшн и мы решали их от месяца до трёх, понимая, что их вызывает, но не понимая как решить и просто плясали с бубном, пока оно не заработало приемлемо» не вызывают оптимизма.
Про уровень hello, world речь не идёт. Там и редкие секундные паузы проблем не вызовут. А скорее всего и их не будет из-за малого количества потребляемой памяти и создаваемых объектов.gagoman
04.01.2017 01:43+2Хороший пример — Cassandra. Написана на Java, кушает себе память и кушает. Конечно, гиганты типа Facebook держат in-house патчи или что-то такое для их объемов, но для большинства — оно работает.
И с каждым улучшением GC Java (вплоть до специально для Cassandra написанного) все пользователи
будут получать улучшение производительности или репортить баг и получать улучшение производительности.
И есть scylladb (не умаляю труда разработчиков), которая год назад только и умела, что сыпать краш дампы.
Для меня лично плюс GC в том, что я уже получаю тысячи часов работы умных людей и могу решать задачу.
А если мне понадобится оптимизировать какое-то место, то есть умные коллеги, которые сделают выбор
free vs delete или, что скорее, скажут заменить O(n^3) на O(n)
lany
04.01.2017 14:48+3Но если б не было сборки мусора, статьи бы не было. Не потому, что проблемы бы не было, а потому что продакшна бы не было. Проект бы просто не вышел на рынок и загнулся из-за нехватки финансирования.
creker
04.01.2017 01:51+3Вам не кажется, что для сложных проектов сборка мусора создаёт большем проблем, чем решает?
Она создает проблемы несравнимо более простые, чем над огромным проектом пыхтеть и трястить за каждый delete, а потом хрен знает сколько отлавливать одни из самых распространенных багов, которые очень сложно найти — работы с памятью. И эти проблемы будут жить с проектом всю его жизнь. Сборщик если и начнет мешать, то скорее всего его придется раз-два настроить, и он будет дальше работать и облегчать работу. Это существенно проще, как по мне, чем ловить утечки и повреждения кучи.AllexIn
04.01.2017 08:19+2А если использовать смартпоинтеры, вместо delete?
mikhanoid
04.01.2017 11:44-3Тогда при каждой операции с ними будут накладные расходы. Чудес не бывает: на управление ресурсами надо затратить опеределённое процессорное время и выполнить какую-то программу. Вопрос в том, кто эту программу напишет, и сколько особенностей Вашего кода она сможет учесть, чтобы работать побыстрее. Не факт, что Вы справитесь в сложных случаях с этой задачей лучше, чем компилятор, который знает о том, что надо собирать мусор.
Хотя, компилятор тоже идеализировать не стоит. На текущий момент хорошего, универсального решения нет.AllexIn
04.01.2017 11:55Накладные операции, в большинстве случаев, будут сравнимы с ручным вызовом delete. Так что ими можно пренебречь.
Речь же о том, почему используется GC, а не смарты? GC заявляется панацеей от необходимости контролировать время жизни объектов. При этом несет много проблем с собой. Смарты тоже убирают проблему контроля времени жизни. При этом дают минимальные накладные расходы и не требуют выполнения в фоне дополнительных плохо контролируемых процессов.mikhanoid
04.01.2017 13:071. Накладные расходы не будут сравнимы. Конечно, зависит от указателя, но при каждой передаче или копировании такого указателя, какие-то операции, скорее всего, атомарные, надо будет совершать. А атомарные операции — штука дорогая. Конечно, можно всем этим распоряжаться аккуратно и учитывать эти накладные расходы. Но зачем?
2. GC — это не только, собственно, контроль за временем жизни. Это ещё и компилятор, который знает, что работает с GC, и старается под этот GC оптимизировать. И это удобнее. Потому что при переписывании кода не нужно заботится о том, что вот здесь мы теперь должны weak_ptr менять на shared, а вот тут shared можем поменять на unique, и наступит счастье. А где-то мы забыли поменять, и счастье не наступило, но мы об этом даже не узнаем, пока production не отвалится. Когда за анализ указателей отвечает компилятор — работать проще.
3. Вообще, по опыту, в реальности, почти всегда приходится писать нечто похожее на GC в каждом чуть более сложном, чем hello world, проекте. У алгоритмов, которые работают со сложными структурами данных, просто карма такая.
4. Ну, и как бы, я не утверждаю, что GC панацея. Серьёзных проблем много и с GC, и без GC. В конце концов, вон ребят из Mozilla так достали умные указатели, что они аж новый язык решили изобрести. Идеального решения на сегодняшний день нет. Но если оно вообще возможно, то всё же, на пути усовершенствования сборщиков мусора и, скорее всего, языков программирования.
konsoletyper
04.01.2017 14:23+1Речь же о том, почему используется GC, а не смарты? GC заявляется панацеей от необходимости контролировать время жизни объектов. При этом несет много проблем с собой. Смарты тоже убирают проблему контроля времени жизни. При этом дают минимальные накладные расходы и не требуют выполнения в фоне дополнительных плохо контролируемых процессов.
Например, потому, что GC всё-таки даёт ощутимо меньшие накладные расходы, чем смартпоинтеры, в том числе по памяти (GC не требуют тащить дополнительные 32 бита на объект для счётчика). Особенно в многопоточном коде. А ещё у смартпоинтеров проблема с циклическими ссылками. А ещё GC уплотняют кучу, этим улучшая локальность доступа к памяти (знаю, современные алгоритмы malloc умеют частично решать проблему фрагментации памяти, но ключевое слово — частично).
В итоге, счётчик ссылок лучше подходит только в достаточно узкой нише, там где очень важно детерминированное время задержки, это всякая кровавая эмбедщина или кровавый хайлоад с каком-нибудь высокочастотном трейдинге. Для остальных задач, коих тьма, GC подходит лучше. Кстати, существуют специализированные GC, которые ценой определённых накладных расходов умеют сильно снижать время задержки. Гуглить например metronome GC.
khim
04.01.2017 16:39Например, потому, что GC всё-таки даёт ощутимо меньшие накладные расходы, чем смартпоинтеры, в том числе по памяти
Это с какого перепугу? Какие-такие накладные расходы при использовании какогого-нибудь unique_ptr?
А ещё у смартпоинтеров проблема с циклическими ссылками.
Госсподя. Далась вам эта проблема. Что, с GC у вас ресурсы утегать не будут? Будут, ещё как будут, если вы запутаетесь в том кто и когда ими владеет. Иначе бы подобные ужасы были бы не нужны. А если вы знаете кто, когда и почему владеет ресурсом, то уж как-нибудь с циклическими ссылками разберётесь, чесслово.
А ещё GC уплотняют кучу, этим улучшая локальность доступа к памяти (знаю, современные алгоритмы malloc умеют частично решать проблему фрагментации памяти, но ключевое слово — частично).
Какое, блин, достижение. А в цифрах — не, не покажите? Что именно вам это даёт? И как часто? С точки зрения конечного пользователя, пожалуйста.
В итоге, счётчик ссылок лучше подходит только в достаточно узкой нише, там где очень важно детерминированное время задержки, это всякая кровавая эмбедщина или кровавый хайлоад с каком-нибудь высокочастотном трейдинге.
Всё как раз наборот. Иначе вот этого бы не было. Поверьте — у Apple есть много недостатков, но умения создать красивые, приятные в работе, программы — у них не отнять. И вот для них — GC противопоказан.konsoletyper
04.01.2017 17:13+2Это с какого перепугу? Какие-такие накладные расходы при использовании какогого-нибудь unique_ptr?
Ну всё-таки давайте будем честны, всю программу на одних только unique_ptr не напишешь. Плюс возникают накладные расходы на создание программы (думать, кто каким объектом владеет). А так, для общего случая нужен указатель с подсчётом ссылок, который добавляет оверхед.
Что, с GC у вас ресурсы утегать не будут? Будут, ещё как будут, если вы запутаетесь в том кто и когда ими владеет.
Будут, но достаточно редко.
Иначе бы подобные ужасы были бы не нужны.
Эти ужасы только в узком слое интеропа с нативной библиотекой UI.
Какое, блин, достижение. А в цифрах — не, не покажите? Что именно вам это даёт? И как часто? С точки зрения конечного пользователя, пожалуйста.
Даёт лучшее попадание данных в кэш. Это, конечно, теоретически, а практически — надо смотреть конкретные ситуации. Мы тут вообще-то холиварим в комментах, а не ведём серьёзную научную дискуссию, поэтому ссылок на конкретные исследования не будет. Могу лишь предложить поделиться ссылкой, которая опровергает мой тезис, т.е. показывает конкретные бенчмарки, где существенного прироста производительности из-за локальности данных, обеспеченных уплотняющим GC, не обнаружено (по сравнению с каким-нибудь dlmalloc).
Всё как раз наборот. Иначе вот этого бы не было. Поверьте — у Apple есть много недостатков, но умения создать красивые, приятные в работе, программы — у них не отнять. И вот для них — GC противопоказан.
Вот это как раз весьма и весьма спорное утверждение. Я так понимаю, вопрос memory management решался ещё во времена objective C, который является по сути макронадстройкой над C, т.е. требуется поддерживать семантику C, которая не предполагает наличие специальным образом сделанного рантайма. Отсюда, единственным возможным выбором для Objective C является консервативный GC, который, как известно, не умеет инкрементальной и параллельной сборки, т.е. тормозит.
mikhanoid
04.01.2017 17:28Ссылка: мужики зарубились на задаче по обработке Англо-Китайского словаря.
По производительности у них одинаково получилось. Но код со сборкой мусора сожрал на 5.5Mb больше.konsoletyper
04.01.2017 17:55Ну так это сравнение между ручным и автоматическим управлением памятью. Подсчёт ссылок — это тоже вид автоматического управления памятью. И с ним у них был бы подобный оверхед по памяти. И вызван он был не GC, а тем, как представлены строки в CLR. А мой вопрос стоял не относительно памяти, а относительно влияния трассирующего уплотняющего GC на (пространственную) локальность, и как следствие — на производительность.
khim
04.01.2017 17:35+1думать, кто каким объектом владеет
Вы хотите сказать что для языков с GC над этим думать не нужно? А для чего тогда целые библиотеки подобные Guice создают? И в чём их отличие от пресловутых «арен» (ну, кроме того, что выключить GC всё равно нельзя)?
Мы тут вообще-то холиварим в комментах, а не ведём серьёзную научную дискуссию, поэтому ссылок на конкретные исследования не будет.
А тогда о чём мы вообще спорим?
Отсюда, единственным возможным выбором для Objective C является консервативный GC, который, как известно, не умеет инкрементальной и параллельной сборки, т.е. тормозит.
О как. То есть для того, чтобы GC «проявил себя» нужно отказаться от использования наработанных библиотек, инструментов и прочего и, грубо говоря, начать жизнь «с чистого листа». Не много ли на себя берёт вспомогательный, по сути, инструмент?konsoletyper
04.01.2017 17:48-1Вы хотите сказать что для языков с GC над этим думать не нужно? А для чего тогда целые библиотеки подобные Guice создают?
Ну это вообще никак не относится к управлению ресурсами. Это DI-контейнер, нужный для облегчения реализации паттерна IoC, а уж зачем он — это вопрос архитектурный (позволяет грамотнее распределить отвественность между компонентами приложения), и никак к управлению памятью не относится.
А тогда о чём мы вообще спорим?
Ну я где-то читал (точно не помню, но по-моему, это была последняя редакция dragon book), что уплотняющий GC даёт лучшую локальность, чем компенсируется его оверхед. Сейчас конкретных ссылок на конкретные исследования дать не могу.
О как. То есть для того, чтобы GC «проявил себя» нужно отказаться от использования наработанных библиотек, инструментов и прочего и, грубо говоря, начать жизнь «с чистого листа».
Это так для legacy-мира. Но тем же Java и JS по 20 лет, .NET — 15 лет. Для них за это время написана целая куча библиотек, инструментов и "прочего". Опять же, есть средства интеропа нативных библиотек с управляемыми рантаймами. И да, разработчики обёрток, которые для managed-рантаймов заворачивают библиотеки, должны думать о явном освобождении ресурсов. Но 1) разработчики приложений об этом уже не думают 2) постепенно старые нативные библиотеки переписывают под новый рантайм.
khim
04.01.2017 18:44+4никак к управлению памятью не относится
А почему, собственно? Фактически это — те самые арены, которые так не любят некоторые. Только они существуют «в стороне» — и в результате в добавление к ним есть ещё и отдельно живущий (и отдельно жрущий ресурсы) GC.
постепенно старые нативные библиотеки переписывают под новый рантайм
Уже нет. Одно время, когда маркетинг заменил людям разум — в это верили. Но в последнее время — от этого отошли. Есть всякие разные PHP/Python/Ruby и прочее, где о скорости, в общем, не думают. И есть старые добрые нативные библиотеки на C/C++ (может со временем Rust подтянется, хотя пока рано говорить об этом).
Многолетний многомиллиардный эксперимент с Java/.NET — в общем и целом, провалился. Хотя да, последствия мы будем ещё очень долго расхлёбывать…
konsoletyper
04.01.2017 18:48А почему, собственно? Фактически это — те самые арены, которые так не любят некоторые.
Многолетний многомиллиардный эксперимент с Java/.NET — в общем и целом, провалился.Пожалуйста, не пишите глупости, не позорьтесь, если не разбираетесь в предметной области.
PsyHaSTe
09.01.2017 18:32Дотнет провалился? Я что-то пропустил? о_0
khim
09.01.2017 19:54Я что-то пропустил? о_0
Да, упустили.
Дотнет провалился?
Провалился не дотнет. Провалилась стретигия, построенная вокруг него.
Про это писали давно, но есть ещё куча разработчков, которые не понимают, что они живут «внутри пузыря».
Java (и .NET) были задуманы как способ сделать «новый мир», куда постепенно перекочуют все разработчики и, разумеется, тот, кто будет его контролировать — будет контролировать всю индустрию. Сетевой компьютер и Singularity — были очередными изданиями мечты о Lisp-машине.
Вот в этом утопическом мире — всё срастается. Когда у вас один рантайм, когда вам ничего не нужно «прогревать» — да, тогда все эти навороты с байткодом и прочим, возможно, оказываются небесполезными.
Туда были влиты буквально миллиарды, но… не вышел каменный цветок. Когда одна компания надорвалась и обанкрутилась, а другая — тихо озакрыла свой проект, то дальше — пришлось как-то те чудеса, которые были сделаны в результате этой попытки как-то утилизовать.
Так что и Java и .NET — будут ещё долго с нами. Десятилетия. Но — как памятник о неудавшейся попытке и как вечные пожиратели ресурсов.
Ибо мир — остался прежним. С операционками с виртуальной памятью и нативным кодом, многими процессами и контейнерами и т.д. и т.п. А утопия — так и осталась утопией.
А потому — потихоньку эйфорию вокрут .NET и Java сворачивают. Всё больше проектов используют нативный код и всякие pyhton/lua/etc для «склейки». А обломки мечты — жрут ресурсы на наших компьютерах и телефонах.Такой себе горатнный нерв в наших компьютерах.PsyHaSTe
09.01.2017 20:46-1То-то .Net Core один из самых востребованных репозиториев на гитхабе, нугет-пакеты из которого за жалкие месяцы скачало больше 4 миллионов разработчиков, а на дотнете построенно куча энтерпрайза, включая самый популярный программистский сайт.
Я уж молчу про то, сколько в последнее время тенденций пихать JS во все. Победы машинных языков пока не видно, все ниши затолпили в лучшем случай байткод-языки, а в худшем — скриптовые, все эти питоны/JS/Lua/…splav_asv
09.01.2017 21:30Сейчас по сути нет смысла делить на скриптовые и байткод. Почти все скриптовые используют байткод. Вопрос — наличие JIT. Но это уже вопрос реализации, а не языка.
khim
09.01.2017 23:36+1Вопрос — наличие JIT
Нет — это как раз не самое главное. Главное — можно ли их легко сопрягать с другими языками в одном процессе. Java и C# для этого в принципе не предназначены (интересно что когда-то Microsoft это как раз очень хорошо понимал: вспомните J/Direct… но потом эйфория архитекторов победила). Хотя, в конечном итоге, умельцы даже заставляют их дружить вместе в одном процессе — но это всё криво, сложно и достаточно «дорого» с точки зрения расходов ресурсов.
Python или Lua же — изначально предполагают, что они — всего лишь вспомогательное средство, а не «основа основ» — но это же обозначает, что весь пафос про GC и прочее — их не касается: их задача — не пытаться построить «новый мир», а всего лишь облегчить работу с теми частями программы, которые на время работы не сильно влияют. Беда начинается когда кто-то на них тяжёлые вычисления начинает делать…splav_asv
10.01.2017 00:34Отвечал скорее на пренебрежительность высказывания о скриптовых языках.
Что до пафоса и великой идеи… Ведь если забыть про это всё, разница не так и велика. Вокруг сложилась тяжелая экосистема, нет хороших средств линковки с C. Но есть же встраиваемая Java, вполне взаимодействовашая с native библиотеками. В общем это воля архитекторов и лидеров сообществ идти в этом направлении. Но в параллельном мире возможен и другой сценарий.
Еще одна оговорка — для обвязки .net и java языки не очень удобны в роли обвязки.
Но в итоге, воможно, на Android так и получится — обвязка на java, а ядро на native.
PsyHaSTe
10.01.2017 06:46Тут скорее вопрос в том, что скриптовые языки почти всегда не имеют типизации или имеют её в очень слабой форме. Исключением из этого правила могу назвать разве что TypeScript, который очень приятен в обращении. А отсутствие строгой типизации я считаю очень большим минусом, и чем больше и сложнее проект, тем это заметнее. Сама «скриптовость» тут особо не при чем, просто исторически сложилось, что скриптовые языки для уменьшения порога вхождения и простоты так вот устроены. А джит это уже вопрос 10й.
splav_asv
10.01.2017 09:25Тот же PHP движется в сторону типизации. Python тоже начал двигаться в сторону типизации. С другой стороны наблюдается движение в сторону вывода типов. Так что постепенно разница уменьшается.
khim
09.01.2017 23:28+1То-то .Net Core один из самых востребованных репозиториев на гитхабе, нугет-пакеты из которого за жалкие месяцы скачало больше 4 миллионов разработчиков, а на дотнете построенно куча энтерпрайза, включая самый популярный программистский сайт.
И? Похожие слова можно было 20 лет назад сказать про Visual Basic. Половина всех людей, которые считали себя программистами писали на нём разнообразные поделия.
Я уж молчу про то, сколько в последнее время тенденций пихать JS во все.
И это тоже пройдёт. Просто долгое время (до появляения Apple AppStore и Google Play) единственным простым способом «донести» приложение до пользователя был web. Соответственно — выросла куча программистов вот под это извращение человеческого ума.
Победы машинных языков пока не видно, все ниши затолпили в лучшем случай байткод-языки, а в худшем — скриптовые, все эти питоны/JS/Lua/…
Кто говорит про победу машинных языков? «Программистов» на скриптовых языках всегда было больше, чем «настоящих».
Просто всем эти создателям разных примочек под emacs не приходило в голову гордо писать в CV, что они — типа, тоже Software Enginer'ы. Слава богу сейчас их вынесли в отдельюную категорию (Application Engineer), что позволит со временем путаницу разгрести.
Умерла идея все переписать на «правильный» язык и всю систему под него заточить. Уж сколько таких попыток было и сколько их было похоронено — не перечесть. Кроме пресловутых Lisp-машин были и iAPX 432 (с аппаратной поддержкой GC, ура!) и много чего подобного. Ничего не прижилось и никогда не приживётся. Потому что на вопрос «а что делать с уже имеющимися горами ПО» ответа не даёт.
А скриптовые языки… это всего лишь скриптовые языки — они не пытаются делать вид, что они — это целый «новый» мир, а всё — остальное это всего лишь «legacy».mikhanoid
10.01.2017 00:15А чем Вам так не нравятся Lisp-машины? Исторически, именно на Lisp-машинах было воплощено впервые всё то многообразие идей, с которыми мы сейчас имеем дело. Там были GUI, векторная вёрстка, сетевые протоколы, мультимедия и CAD-ы. Вообще, что было на Lisp-машинах в конце 80-ых на PC появилось только в конце 90-ых, и не в полном объёме. Да что там говорить, даже TCP/IP впервые на Lisp-машине появился. Проблема Lisp-машин даже не в технологиях была, а в том, что Symbolic (основной драйвер Lisp-индустрии) втянулась в какую-то аферу с недвижимостью и прогорела. Но осколки Symbolic живы, и часто являются лидерами в своих направлениях. Например, именно благодаря Lisp-машине NaughtyDog умеет делать огромные бесшовные миры для Uncharted (это к вопросу о производительности; они вообще только из-за давления Sony переписали движок на C++, Lisp их вполне устраивал и по скорости, и по возможностям писать низкоуровневый код).
И это только экосистема Lisp. А есть ещё экосистема ML. Тоже с довольно интересной историей и своими текущими успехами, в том числе, в высокопроизводительных системах. Вот Вы когда заходите на https://mirage.io/, почему, как Вам кажется, оно тормозит? А вот и нет. Потому что они стартуют виртуальную машину с runtime ML внутри под конкретно Вашу сессию.
И есть ещё экосистема Haskell. Там тоже интересно.
Это я всё к чему? К тому, что идея языковой операционной среды жизнеспособна. Тем более, в наше время. Никому не интересно, C++ в приложении поверх Linux, или же Lisp на голом железе, главное, чтобы на JSON и HTTP умел общаться с окружающим миром.
Ну, то есть, скорее всего, ждёт нас эпоха мультикультурализма в плане языков программирования. Будут экосистемы с разными базовыми языками (тут вот, например, GNU решила Guile Sheme допилить до VM и реализовать поверх неё Python, JS, Ruby и Lua), ну, а программист уже будет просто выбирать в какой экосистеме ему комфортнее.
Экосистема С/С++ вряд ли куда-то пропадёт с этого праздника жизни и разнообразия, но возвращение Lisp-машин, или появление униядер ML, или мощных runtime-ов Go вполне, мне кажется, имеет смысл приветствовать.
Повторюсь, людям это интересно, и это не давление сверху со стороны SUN или Microsoft, просто людям так эффективнее работается. Хорошо же.khim
10.01.2017 00:32+1А чем Вам так не нравятся Lisp-машины?
Они просто наиболее известны. А так — да, попыток было много.
К тому, что идея языковой операционной среды жизнеспособна.
Пока что все попытки кончались тем, чем и должны были кончаться — несколько лет шумихи, пока вкладываются деньги, потом — возврат на С, Fortran и прочее.
тут вот, например, GNU решила Guile Sheme допилить до VM и реализовать поверх неё Python, JS, Ruby и Lua
Ага. И продемонстрировала proof-of-concept с поддержкой scheme и tcl в 1995 году. На календарь давно смотрели?
ну, а программист уже будет просто выбирать в какой экосистеме ему комфортнее
Ох уж эти сказки, ух уж эти сказочники. В том-то и дело, что эти попытки — «ходят по кругу»: сначала за короткое время делается proof-of-concept, дальше с ним носятся, как с писанной торбой, потом — туда вбухивается куча денег (иногда — миллионы, иногда — миллиарды, как повезёт), а потом… финансирование кончается.
И да — побочные эффекты часто двигают индустрию вперёд. А вот «философский камень» всё как-то не вырисоывается… Такая «современная алхимия»…
Повторюсь, людям это интересно, и это не давление сверху со стороны SUN или Microsoft, просто людям так эффективнее работается. Хорошо же.
Чего хорошего в том, что люди пишут 100 парсеров JSON'а и 500 http-серверов? Хорошо хоть TCP/IP стек системный используют.
Лучше бы вместо того, чтобы разрабатывать 100500 изолированных миров подумали бы о том как делать систему в которой можно программировать на разных языках — но почему-то все попытки так сделать приводят только к ещё одному монстру, который пытается в себя втянуть весь мир…mikhanoid
10.01.2017 01:19Они просто наиболее известны. А так — да, попыток было много.
Так в том-то и дело, что Lisp-машины не были «попыткой». Это было развитое направление в индустрии. И развивались они быстрее, чем UNIX и Си. И успехов там было много. Люди вот об этом помнят. Поэтому и стремятся повторить успех. Тем более, на новом уровне производительности и технологий можно более интересные вещи делать.
Ага. И продемонстрировала proof-of-concept с поддержкой scheme и tcl в 1995 году. На календарь давно смотрели?
И что? Non-profit же. Хотят — делают, хотят — не делают. Guile вообще долго не развивался, но пару лет назад за него снова взялись.
И да — побочные эффекты часто двигают индустрию вперёд. А вот «философский камень» всё как-то не вырисоывается… Такая «современная алхимия»…
Да никто там не ищет «философского камня». Просто есть проблемы, есть подходы к их решениям, есть люди, которые верят в эти подходы и работают над ними. Си/Си++ же тоже не идеальны. Да, код быстрый, но работать с динамическими данными тяжело. Если на Java можно написать WebSphere, а на Erlang ту же Riak, то можно ли это сделать на Си++? Ну, не знаю. Никто почему-то не сделал. Почему?
Чего хорошего в том, что люди пишут 100 парсеров JSON'а и 500 http-серверов? Хорошо хоть TCP/IP стек системный используют.
Лучше бы вместо того, чтобы разрабатывать 100500 изолированных миров подумали бы о том как делать систему в которой можно программировать на разных языках — но почему-то все попытки так сделать приводят только к ещё одному монстру, который пытается в себя втянуть весь мир…
А люди и так напишут 100 парсеров JSON-а, даже если у них будет один язык программирования. А потом напишут ещё парсеры для других языков программирования. И так далее. Ну, и, люди любят вообще-то искать свою идентичность в каких-нибудь малых группах. Мы вообще не приспособлены ощущать себя хорошо в многомиллионных однообразных коллективах. Поэтому вот так вот.
Но я не понимаю, а чего Вы так переживаете? Вряд ли экосистема UNIX/Си в ближайшее время куда-нибудь пропадёт. Работы в ней для всех хватит. Маловероятно, что Intel, Apple или Google накроются медным тазом и GCC или LLVM останутся без финансирования и остановятся в развитии.khim
10.01.2017 01:40+1Поэтому и стремятся повторить успех.
Нельзя дважды войти в одну и ту же реку. Вопрос насчёт «успеха» — тоже отдельный, но даже если признать что он таки был — время несовместимых решений ушло.
Если на Java можно написать WebSphere, а на Erlang ту же Riak, то можно ли это сделать на Си++? Ну, не знаю. Никто почему-то не сделал. Почему?
А что именно вы понимаете под «WebSphere под Си++»? Чем он будет отличаться от Docker?
Но я не понимаю, а чего Вы так переживаете?
Потому что наследием всего этого является совершенно нерациональное расходование ресурсов. Что особенно заметно на Android'е, но и не только на нём (и было бы ещё заметенее если бы в iOS разработчики не всовывали всякими правдами неправдами всяких монстров типа Mono).mikhanoid
10.01.2017 02:06Но почему нерациональное? Основной мотивацией порыва SUN в сторону Java была же простое желание: даль сложному софту работать на более простых процессорах. И дело даже не в чайниках, а в том, что они посчитали: их сервра под нагрузкой тратят 30% времени на обслуживание виртуальной памяти. Это без подкачки, просто на управление TLB, создание областей разделяемой памяти, fork-и и прочие такие вещи. У меня где-то статья даже эта завалялась в коллекции. Мотивация у Java — это более рациональное использование ресурсов: зачем проверять каждый доступ в память, если можно гарантировать, что они всегда будут верными?
khim
10.01.2017 04:49+1Но почему нерациональное?
Потому что лишняя работа ещё никогда и никем забесплатно не делалась.
Мотивация у Java — это более рациональное использование ресурсов: зачем проверять каждый доступ в память, если можно гарантировать, что они всегда будут верными?
Да, идея была красивой. Но… не работает. Без аппаратной поддержки — не работает. Вот, собственно, официальная капитуляция.
Java вообще и сборка мусора в частности — это прекрасная иллюстрация известного принципа: на всякий сложный вопрос имеется ответ – ясный, простой и ошибочный.
И дело даже не в чайниках, а в том, что они посчитали: их сервера под нагрузкой тратят 30% времени на обслуживание виртуальной памяти.
Ну да. А сколько они тратят на обслуживание двух видов виртуальной памяти друг над другом??
В том-то и дело, что это было решение не той задачи, не там и не для того. В узких нишах — да, это решение применимо (собственно меня удивляет что вы этого идейного потомка Лисп-машин на божий свет не вытащили), но как универсальная платформа — оно не годится. Просто потому что проигрывает в скорости другим подходам.
P.S. И тут ещё нужно учесть что любое «узкое решение» со временем стремится стать «широким». На тех же AS/400 сейчас часто гоняется какой-нибудь PHP, что в результате стоит дороже, чем гонять его на обычных серверах — но если вы уже завязались на AS/400, то вам приходится жрать этот кактус…PsyHaSTe
10.01.2017 06:55Не вижу ничего провалившегося ни в дотнете, ни в джаве. Тем более, что по-настоящему нагруженные проекты, где нужна масштабируемость и производительность пишут именно на джаве, а не на С/С++. Наверное потому что они такие быстрые, а джава — медленная. Или все это заговор ЛПРов с Ораклом.
mikhanoid
10.01.2017 11:32Да, идея была красивой. Но… не работает. Без аппаратной поддержки — не работает. Вот, собственно, официальная капитуляция.
А в чём Вы видите факт капитуляции? Ну да, на поддержку Applet-ов забивают постепенно, потому что есть JavaScript. Вполне естественно, об этому говорили ещё тогда, когда V8 только появился.
Java вообще и сборка мусора в частности — это прекрасная иллюстрация известного принципа: на всякий сложный вопрос имеется ответ – ясный, простой и ошибочный.
Сборка мусора никогда не была ясным и простым ответом на проблему. Ясный и простой ответ — это просто запретить свободную динамическую память, как в Fortran. Всегда было понятно, что сборка мусора — штука сложная и требует много усилий для реализации. Но так же было понятно, что во многих сценариях она необходима.
Тут же дело какое. Сборка мусора — это не просто способ управления памятью, это, действительно, техника которая влияет на всю систему конкретного языка. Если сборки мусора нет, то библиотеки надо писать одним способом, а если есть, то можно писать другим. И со сборкой мусора библиотеки могут быть гораздо более гибкими. А это тоже важно, если программист может не тратить 50% своего времени на продумывание схемы управления ресурсами, а сразу реализовывать алгоритм.
На Си++ тоже, наверное, можно сразу сесть за реализацию, ничего не продумывая в плане потока ресурсов. Но лично мой опыт показывает, что потом 10 раз придётся всё переписывать.
Скорость разработки тоже штука важная. И это было понятно ещё во времена Алгол-68.
Ну да. А сколько они тратят на обслуживание двух видов виртуальной памяти друг над другом??
В смысле? В безопасных языках нет виртуальной памяти. На том и стоят. А когда они работают с виртуальной памятью, то продвинутые системы типа .Net или Java пытаются её задействовать в свою пользу. Например, используют cow-отображения страниц при копировании больших объектов. Можно так сделать на Си++? Можно. Но кто же так будет делать? На Си++ скорее всего, человек просто наплодит адову схему из перекрёстных ссылок.
И тут возникает вопрос, а что считать стоимостью? Ведь, код пишут люди. Люди пишут через пень-колоду. И не факт, что та схема управления ресурсами, которую написал Василий, будет эффективнее, чем сборка мусора. Программы-то сложные.
Кроме того, никто не отменял техники оптимизации. Можно оптимизировать код со знанием того, что он будет работать со сборкой мусора. Пишут же люди довольно быстрые игры на Java. Просто выделяют несколько больших массивов для хранения всего необходимого и сборка мусора их почти не касается.
Просто потому что проигрывает в скорости другим подходам.
Нет объективных данных, что проигрывает. Проблема со сборкой мусора не в проигрыше вообще по скорости, а в том, что есть проблема непредсказуемых задержек. В среднем, в пакетных режимах, сборка мусора забирает до 5% времени (в равнении с виртуальной памятью — копейки), в зависимости от задачи.
Проблема именно с непредсказуемыми задержками. Но опять же. Когда всё живёт поверх виртуальной памяти, то никаких гарантий на задержки тоже нет. TLB будет вносить неопределённости, операционка будет дефрагментацией заниматься, cow-процессы будут происходить.
Автоматически от использования Си++ проблема не исчезнет. Для гарантии задержек нужно будет специальным образом писать код и специальным образом резервировать ресурсы. Но в точности такое же можно делать и в системах со сборкой мусора. Там есть технологии realtime-пулов памяти и всего такого прочего.
Гарантированных времён реакции системы можно добится и с одной техникой, и с другой. В конце концов всё упирается в умения и навыки программиста. И это очень важный экономический фактор.
Ну, а здесь, как ни крути, Си++ Java совсем не конкурент. Даже на Haskell писать проще, чем на Си++. Потому что на Си++ элементарно отстреливаешь себе все конечности, и даже не знаешь, что именно оказывается отстреленным. Отладка больших Си++ программ — вот истинный источник боли и унижения в современном мире.khim
10.01.2017 13:11+1А в чём Вы видите факт капитуляции?
Подписанные апплеты не используют механизмы Java для обеспечения безопасности. В частности они могут загружать нативные библиотеки. Рассказать куда можно засунуть все ваши проверки если в Java-машину загружена нативная библиотека через JNI или сами догадаетесь?
Но так же было понятно, что во многих сценариях она необходима.
Совершенно непонятно. Чуть ли не основной повод для сборки мусора — это усложнение интерфейсов. Что такого в интерфейсах появилось со времён какого-нибудь Visual Studio 6, что вдруг потребовало сборки мусора?
В смысле? В безопасных языках нет виртуальной памяти.
Так в том-то и дело, что нет безопасных языков в природе почти. Просто… нет. История про Java — описана выше. Вот вам про python.
То есть сдалать-то безопасный язык можно… а потом добавляются фичи, ещё фичи, примочки, хотелочки… и оп-па: язык у вас, оказывается — небезопасный. Его, оказывается, нужно на уровне операционной системы ограничивать. А тогда, извините, за что мы, собственно, платим?
И не факт, что та схема управления ресурсами, которую написал Василий, будет эффективнее, чем сборка мусора.
Факт. Причём достаточно очевидный факт.
Программы-то сложные.
А вот тут — в точку. Программа — это газ. И её сложность — тоже. Вася не сможет реализовать менее эффективную схему управления/ ресурсами на языке без GC. Просто не сможет. Его программа — будет падать и всё.
Вопрос не стоит как «а будут ли лучше программы, написанные на языке без GC», а как «компенсирует ли ускорение разработки ухудшение качества программа вызванное использованием GC?».
Пишут же люди довольно быстрые игры на Java. Просто выделяют несколько больших массивов для хранения всего необходимого и сборка мусора их почти не касается.
Действительно: а чёб нам не ввести подсистему, которая должна нам всё «улучшить», а потом начать её героически преодолевать? И всё равно GC нет-нет да отжирает свои ресурсы, так как отключить его полностью в Java нельзя.
В среднем, в пакетных режимах, сборка мусора забирает до 5% времени (в равнении с виртуальной памятью — копейки), в зависимости от задачи.
В том-то и дело, что не «в равнении», а в добавление! «В равнении» — это в очень ограниченных узкоспециализированных системах на ограниченном промежутке времени.
Отладка больших Си++ программ — вот истинный источник боли и унижения в современном мире.
А это уж «кто на что учился», извините. Грамотно написанная программа никакой особой боли не вызывает, не больше чем программа на Java. А неграмотно — тут да. Программа на Java, написанная кривыми руками обеспечит целое пополение «условных индусов» работой на годы вперёд. Подобная же программа на C++ — просто не заработает. Но является ли это недостатоком Java и проблемой C++? Зависит от ваших задач…mikhanoid
10.01.2017 14:23Так в том-то и дело, что нет безопасных языков в природе почти. Просто… нет. История про Java — описана выше. Вот вам про python.
То есть сдалать-то безопасный язык можно… а потом добавляются фичи, ещё фичи, примочки, хотелочки… и оп-па: язык у вас, оказывается — небезопасный. Его, оказывается, нужно на уровне операционной системы ограничивать. А тогда, извините, за что мы, собственно, платим?
Безопасные языки есть. И есть безопасные системы. Я уже приводил пример MirageOS. Unsafe-режимы нужны, в основном, именно для того, чтобы вписываться в существующие native-экосистемы или использовать native-библиотеки, потому что родных нет. Если такой необходимости нет, то можно написать операционку типа House, и жить целиком на Haskell, на одной дискете и не испытывать проблем с производительностью.
Native — это не технологическая необходимость, а экономическая. Я уже говорил, что да, действительно, это важный фактор. И моя точка зрения заключается в том, что именно он самый важный фактор в устойчивости текущих основанных на Си/Си++ программных экосистем. Просто кода понаписано столько, что его уже не перенести никуда за разумные деньги. Не в скорости тут дело совсем. Уже довольно долго существуют безопасные системы, которые по скорости исполняемого кода уделывают Си/Си++. ATS, например.
А вот тут — в точку. Программа — это газ. И её сложность — тоже. Вася не сможет реализовать менее эффективную схему управления/ ресурсами на языке без GC. Просто не сможет. Его программа — будет падать и всё.
Да ладно!? Такое ощущение, что Вы никогда не сталкивались с говнокодом. У меня был случай, когда нам пришлось искать баг в программе, в которой каждый день утекал один мегабайт данных. Мы нашли места утечек при помощи Valgrind. Мы даже поняли, почему так происходит. Но исправить ничего у нас не получилось, потому что мы не смогли переписать код, там сотни тысяч строк кода и наивные попытки просто его роняли. Можно было бы зарефакторить всё, но никто бы нам этого не оплатил. В итоге, заказчик просто каждый день стал перезапускать приложение. Вот Вам и эффективность.
При этом, программа вполне себе работала два месяца подряд, потом падала, потом снова работала.
И я сомневаюсь, что это нетипичная ситуация.
Действительно: а чёб нам не ввести подсистему, которая должна нам всё «улучшить», а потом начать её героически преодолевать? И всё равно GC нет-нет да отжирает свои ресурсы, так как отключить его полностью в Java нельзя.
Да никто и никогда не считал GC «улучшением». GC — это необходимое зло в безопасных системах. И с ним точно так же, как скажем, с количеством системных вызовов в Си-программах, или с количеством синхронизаций в параллельных программах яростно сражаются, пытаясь всячески развить и компиляторы, чтобы точно вычислять регионы использования ресурсов, и придумать способы эффективного программирования.
Борьба идёт именно за безопасность, а не за сборку мусора. Сможете предложить, как делать безопасный код с ручным управлением памятью — сообщество возрадуется. Но, к сожалению, это принципиально невозможно.
А это уж «кто на что учился», извините. Грамотно написанная программа никакой особой боли не вызывает, не больше чем программа на Java. А неграмотно — тут да. Программа на Java, написанная кривыми руками обеспечит целое пополение «условных индусов» работой на годы вперёд. Подобная же программа на C++ — просто не заработает. Но является ли это недостатоком Java и проблемой C++? Зависит от ваших задач…
Ещё как заработает! Нет, ну в самом деле. Вон в соседней статье обсуждают косяки в коде MySQL.
Кстати. Вы сужаете тему принудительно к Java против C++. Java, действительно, не является идеальным языком с точки зрения надёжности, безопасности и технологической продвинутости. Тут бы со Scala надо сравнивать.
Но есть и другие языки, в которых всё намного лучше. А сама технология безопасных языков и language-level os развивается. Пока в академическом сообществе. Может быть, она там останется навсегда. А, может быть, и не останется. Уж слишком хорошо иногда получается. Снова упомяну ATS и Arrp.
fogone
10.01.2017 14:42-1Языки со сборкой мусора не только не потерпели фиаско, но и более того завоевывают всё большую популярность (что можно не сложно понять, посмотрев рейтинги их использования), в отличии от языков с ручным управлением памятью, которые всё чаще применяются в очень ограниченном количестве кейзов. И это не удивительно, потому что писать приложения, где памятью по большей частью управляет умная система, которая сама очистит больше не используемую память, проще и удобнее, чем делать это вручную. С этим очевидным фактом будет спорить только человек, который или ничего не понимает в разработке приложений для реальной жизни или застрявший (восприятием кулика) в своей узкой области, в которой действительно жить со сборкой мусора просто невозможно или доставляет больше боли, чем дает профитов. Более того, в тех областях, где казалось бы использование сборки мусора кажется странным, она тем не менее периодически используется: например, UnrealEngine — один из самых популярных игровых движков на которых делают игры класса AAA, написан на C++ и имеет свою систему сборки мусора, чем нимало никто не расстроен, а даже где-то наоборот. А тот факт, что сборка мусора не нравится именно вам, не делает языки её использующие ни хуже ни лучше. Еще раз хочу отметить очевидное: есть класс задач, для которых использование сборки мусора очевидно не подходит, однако для большого числа задач, это решение делает разработку проще и комфортнее, пусть за это и приходится платить известную цену.
mikhanoid
09.01.2017 22:04Эта идея вечна и постепенно воплотится в жизни. Это интересный challenge и люди не сдадутся. Да, может быть, пока это обратно всё ушло в НИР, но там прогресс довольно существенный.
Советую вам ради интереса посмотреть на ICFP-2015 и ICFP-2016. Там довольно большая движуха именно на эту тему. Очень много объективно крутых проектов.
Они пока ещё не вписаны в общую экосистему, но по отдельности впечатляют. Arrp какой-нибудь с глобальной оптимизацией и вращениями вычислений над многомерными массивами, и автоматическим параллелизмом (TensorFlow тихонько завидует в сторонке). Или тот же Ur/Web.
Сейчас уровней виртуализации очень много. Чрезмерно. Все с этим пытаются бороться. Не факт, что выйдет нечто годное. Но не факт, что и не выйдет.
Не очевидно, кстати, что SUN погорела именно из-за Java. У них много косяков в стратегии было.khim
09.01.2017 23:45+2Не очевидно, кстати, что SUN погорела именно из-за Java.
Никакого отношения к проблемам SUN'а Java не имеет. Проблема SUN'а была в том, что сделать «новый мир» не удалось, а на попопытку его сотворить ушли все наличные ресурсы.
В своё время S/360 была похожим проектом — но там дело выгорело по двум причинам:
1. У IBM был больше «запас прочности».
2. Они изначально сделали ставку на железо и очень жёстко пресекали всех желающих выпускать совместимые модели.
В результате SUN — со своим «новым миром» кончился, а IBM железяки выпускает и продаёт до сих пор.
creker
04.01.2017 17:29-1Всё как раз наборот. Иначе вот этого бы не было. Поверьте — у Apple есть много недостатков, но умения создать красивые, приятные в работе, программы — у них не отнять. И вот для них — GC противопоказан.
У Apple как раз GC и был. Потом уже начали съезжать на другое. Как раз для UI GC отлично работает, ибо пауз в работе полным полно, которые никто не заметит. Да и GC нынче в фоне работают параллельно коду. А вот какой-нить серверный код высоконагруженный требует довольно много от GC, ибо там паузы будут очень сильно заметны.khim
04.01.2017 17:39+3У Apple как раз GC и был.
Была экспериментальная поддержка — только в MacOS. В iOS — не было никогда и даже на десктопе эксперимент был признан неудачным.
Как раз для UI GC отлично работает, ибо пауз в работе полным полно, которые никто не заметит.
Только если у вас Ынтырпрайз приложение, за использование которого людям деньги платят. В противном случае — вам нужно реагировать за пресловутые 16 миллисекунд.
А вот какой-нить серверный код высоконагруженный требует довольно много от GC, ибо там паузы будут очень сильно заметны.
Вы это серьёзно? Много вы серверных приложений видели, гарантированно дающих ответ за 16 миллисекунд? Разве что на бирже…creker
04.01.2017 17:46-1Только если у вас Ынтырпрайз приложение, за использование которого людям деньги платят. В противном случае — вам нужно реагировать за пресловутые 16 миллисекунд.
И с GC нормальным оно будет прекрасно реагировать, потому что stop-the-world практически не будет происходит. Все будет в фоне. Такое ощущение, что у вас GC на несколько секунд всю программу вешает. Может у Apple оно так и было, но у нормальных GC реализаций с этим все в полном порядке.
Вы это серьёзно? Много вы серверных приложений видели, гарантированно дающих ответ за 16 миллисекунд? Разве что на бирже…
Серьезно настолько, что даже в Go над этим до сих пор трудятся и правят перодически всплывающие хитрые кейсы, когда гарантии задержек уплывают и это сразу фиксят. Чрезвычайно важны минимальные задержки и их постоянство. Это вам не UI, где пока человек моргнет, уже весь мусор собран будет.khim
04.01.2017 18:47+1Отличный ответ.
Может у Apple оно так и было, но у нормальных GC реализаций с этим все в полном порядке.
...Серьезно настолько, что даже в Go над этим до сих пор трудятся и правят перодически всплывающие хитрые кейсы, когда гарантии задержек уплывают и это сразу фиксят.
Так «всё в полном порядке» или «над этим до сих пор трудятся»?
«Старую песню о главном» про то, что вот ещё чуть-чуть и GC перестанет быть проблемой — я слышу сколько я программистом работаю, уже не один десяток лет. А воз и ныне там.
ik62
04.01.2017 19:16+1Нода кассандры, примерно 2.5к запросов в секунду — 99-я перцентиль — 2.5ms. Это не то-же самое что «гарантированно», но для практических целей — достаточно.
0xd34df00d
04.01.2017 18:33Например, потому, что GC всё-таки даёт ощутимо меньшие накладные расходы, чем смартпоинтеры, в том числе по памяти (GC не требуют тащить дополнительные 32 бита на объект для счётчика).
И при уплотнении тоже никакого оверхеда?
Особенно в многопоточном коде.
Случаев, когда нужно именно разделить владение, да прямо между работающими тредами, на мой взгляд, достаточно мало. Во всех остальных случаях можно передать shared_ptr по ссылке (никаких атомарных операций, так как никакого копирования), либо вообще передать сырой указатель, если контракт таков, что объект должен жить всё время работы вызываемого кода, и это гарантируется прочими способами.
И, кстати, когда-то давно в одном из проектов я писал свой отдельный per-request-pool, который выделял где-то с десяток мегабайт при инициализации, сдвигал указатель вперёд при выделении памяти, ничего не делал при освобождении, и прибивал всю память одним махом при завершении обрабокти запроса. Интегрально стоимость выделения — инкремент указателя + стоимость конструктора. Стоимость освобождения — стоимость деструктора. Никаких локов, ничего, не нужно оно там. GC правда может эффективнее?
А ещё GC уплотняют кучу, этим улучшая локальность доступа к памяти
Там, где действительно нужна локальность, обычно и так выделенный ручками массив из каких-нибудь uint32_t или упакованных структур.
konsoletyper
04.01.2017 18:54+1И при уплотнении тоже никакого оверхеда?
При уплотнении есть оверхед. Другое дело, что GC не обязательно должен быть уплотняющим. Но уж так сложилось, что для передовых рантаймов пишут уплотняющие GC, наверное, практика показала, что они эффективнее. Потому что так аллокация сильно дешевле (по сути — обычный инкремент), и потому что расходы на уплотнение компенсируются выигрышами от пространственной локальности.
Никаких локов, ничего, не нужно оно там. GC правда может эффективнее?
Кстати, я где-то видел презентацию, где в Graal добавили region-based memory management, и не получили ощутимого прироста производительности. GC не может эффективнее, но может в ряде случаев приближаться по эффективности. По крайней мере, аллокация и освобождение столь же дешёвые, минус расходы на обход графа объектов.
0xd34df00d
04.01.2017 20:10+2Другое дело, что GC не обязательно должен быть уплотняющим. Но уж так сложилось, что для передовых рантаймов пишут уплотняющие GC, наверное, практика показала, что они эффективнее.
Так давайте определимся, уплотняющий GC или нет. А то получается, что преимущества приводятся для их объединения.
потому что расходы на уплотнение компенсируются выигрышами от пространственной локальности
Вот вы говорите: пространственная локальность, пространственная локальность. А откуда вообще следует, что она, ну, есть? Ведь для пространственной локальности необходимо, чтобы, условно, следующий объект в памяти, к которому вы обращаетесь, лежал в вашем кешлайне. Как это связано с компактифицирующим GC?
С generational GC, да, связано, но не с компактифицирующим.
По крайней мере, аллокация и освобождение столь же дешёвые, минус расходы на обход графа объектов.
В вышеупомянутом подходе их вообще нет.
konsoletyper
05.01.2017 13:26Так давайте определимся, уплотняющий GC или нет. А то получается, что преимущества приводятся для их объединения.
В современных рантаймах GC уплотняющие, и это подаётся как преимущество, а не как недостаток, вопреки тому, что уплотнение даёт оверхед.
А откуда вообще следует, что она, ну, есть? Ведь для пространственной локальности необходимо, чтобы, условно, следующий объект в памяти, к которому вы обращаетесь, лежал в вашем кешлайне. Как это связано с компактифицирующим GC?
Собственно, причины две. Во-первых, в survival space объекты будут лежать кучно, и они могут взять и влезть в кэш. Во-вторых, при выделении памяти в eden space она будет всегда выделяться строго последовательно, а не в дырках.
С generational GC, да, связано, но не с компактифицирующим.
А поколения тут при чём? Поколения нужны, чтобы уменьшить паузы и увеличить пропускную способность за счёт обхода только подмножества графа. Или я что-то не понимаю?
В вышеупомянутом подходе их вообще нет.
Это понятно, что в определённых ситуациях можно вручную управлять памятью эффективнее, чем GC. Но во-первых, это отнюдь не те пресловутые 95% ПО, а во-вторых, это не всегда даёт достаточный выигрыш, чтобы оправдать увеличение стоимости разработки.
Так, иногда и компиляторы идут лесом и код, написанный вручную на ассемблере, оказывается быстрее. Например, интерпретатор Java-байткода за счёт грамотного распределения регистров аж в 2 раза быстрее работает, если его написать на ассемблере. Но в большинстве случаев компилятор тупо умнее программиста.
chabapok
05.01.2017 04:34+1выделял где-то с десяток мегабайт при инициализации, сдвигал указатель вперёд при выделении памяти, ничего не делал при освобождении, и прибивал всю память одним махом при завершении обрабокти запроса.
Я вас поздравляю: вы реализовали eden область java-вского хипа.
поэтому,
GC правда может эффективнее?
нет, он просто уже делает из коробки то, что вам пришлось изобретать. «Всяк велосипедостроитель свой велик хвалит»0xd34df00d
05.01.2017 06:59+1И он автоматом понимает, что на каждый запрос достаточно иметь свою область в десяток мегабайт? И что так как это локально для запроса, а запрос в каждый момент обрабатывается одним тредом, то блокировки не нужны совсем? И что при завершении запроса можно вообще всю память грохнуть, не проходя по графу объектов? И что если запрос всё ещё обрабатывается, то память трогать не надо, что бы в других запросах не происходило? И ещё, небось, никакого оверхеда, кроме действительно той памяти, что была выделена, нет?
chabapok
05.01.2017 15:43И он автоматом понимает, что на каждый запрос достаточно иметь свою область в десяток мегабайт?
это несущественная особенность реализации. А я говорю о принципе. Понятно, что оно там написано не точно так же как у вас. Eden у него там фиксированного размера, можно ключиками запуска регулировать.
в каждый момент обрабатывается одним тредом, то блокировки не нужны совсем?
Да, на каждый поток своя область памяти, поэтому создать и бросить объект — так же быстро, как и у вас. А если объект выживает, и ссылка на него пошла по потокам(или даже не по потокам) — то потом gc переносит его в другую область.
И что при завершении запроса можно вообще всю память грохнуть, не проходя по графу объектов?
да, насколько понимаю. Тут понятно, что как-то выжившие объекты выдергиваются и переносятся в другую область. Но скорей всего, это происходит не проходом «в лоб», в том понимании, которое вы подразумевали. Процесс этот достаточно быстр. И в G1, насколько понимаю, может быть просто выбрана другая область, если в данной что-то пооставалось. Но тут инфа не 100%. Как оно там точно сделано в деталях — не знаю.
если запрос всё ещё обрабатывается, то память трогать не надо, что бы в других запросах не происходило?
Если обработка другим потоком — то может и не затронуть, а если этим же потоком, то объекты перекочуют в другую область памяти, где уплотнение происходит другим алгоритмом.
И ещё, небось, никакого оверхеда, кроме действительно той памяти, что была выделена, нет?
тут смотря как считать и что считать оверхедом. Однозначно на этот вопрос ответить невозможно. Тут так же надо понимать, что когда ты делаешь malloc() — то есть тоже некоторый «оверхед» который нужен для free. И поскольку алгоритм eden нельзя использовать для всего — оверхедом тоже можно принебрегать только в узком круге задач. В java каждый объект наследуется от Object, а он в зависимости от jvm и архитектуры занимает от 12 байт (не считая всяких внутренних классов, которые заоптимайзены и могут занимать меньше). И в этих 12 байт, насколько понимаю — значительная часть того же оверхеда, который в сишном хипе используется для free. Поэтому говорить, мол «в java на каждый обьект накладные расходы 12 байт, а в сишном хипе на каждый malloc ноль накладных» — некорректно.
Разумеется, ваш специализированный хип будет более оптимальным в этом смысле.
А фактически — у вас свой велосипед, который является обрезаной и доработаной для вашей задачи версией существующего.
Если говорить принципиально — оно там одинаковое, различие только в деталях, которые в той или иной задаче дадут небольшой выигрыш или проигрыш.
vsb
04.01.2017 15:18+1Сегодня 3 основные техники управления памятью.
1. Всё руками. Это C. Плюс — накладные расходы такие, какие мы хотим. Минус — легко допустить ошибку.
2. Автоматический подсчёт числа указателей. Это Objective C и Swift. Плюс — минимум внимания для работы с памятью. Минусы — легко допустить утечку памяти из-за циклических ссылок; накладные расходы на каждое изменение указателя, причём для корректной работы в условиях мультипоточности ещё и определённые барьеры нужны.
3. GC. Плюсы — всё работает само. Минусов хватает, но для большинства программ плюс перевешивает.
Rust можно считать новой парадигмой, это разновидность пункта 1, но компилятор гарантирует отсутствие ошибок при работе с памятью за счёт урезания возможностей программиста и сложной системы типов.
C++ теоретически позволяет то же самое, что и Rust, но тут вместо гарантий компилятора должна быть самодисциплина программиста. Сложно сказать, что лучше.AllexIn
04.01.2017 15:20-2А разве GC научились уверенно распознавать циклические ссылки? Я видимо пропустил этот момент.
jreznot
04.01.2017 16:36+3Все GC в Java определяют достижимость объектов от корней кучи. Если объект недостижим — он кандидат на отстрел, циклические ссылки не могут препятствовать отстрелу по определению.
fly_style
05.01.2017 18:31https://www.youtube.com/watch?v=3UP0o2gkeRQ — вот вам доклад, советую ознакомиться :)
Nakosika
04.01.2017 09:13-1Грааль сборщика мусора в том, что он должен происходить на уровне железа.
asdf87
04.01.2017 14:44Как вы себе это представляете, когда оптимальный алгоритм GC зависит от решаемой задачи? В этом же и проблема, что универсального решения еще не нашли. Как только найдут, то, думаю, практически сразу появятся такие харварные решения.
Nakosika
05.01.2017 10:49+1Нормально представляю. Все сложности, описанные в статье, решаемы, и на уровне железа проще. Про хардварную реализации я у Кармака впервые идею услышал где-то пол года назад.
mikhanoid
04.01.2017 11:39+9На практике это всё вопрос тяжёлого многомерного компромисса. Вот некоторые оси проблемы.
1. Чем сложнее проекты и алгоритмы, тем потребность в сборке мусора выше. Обычно, в сложном коде приходится вводить какую-нибудь сборку мусора. Например, выдумывать арены.
2. Rust проблему решает, но за счёт существенного увеличения сложности программирования. Первая версия кода пишется почти без проблем. Но дальнейшее перепроектирование и переписывание алгоритмов управления ресурсами даются непросто. Да и потом, нормальный статический анализ кода может все эти эффекты просто вычислить.
3. Безопасность. Это тоже очень важно. Одновременно безопасности и достаточного уровня сложности без сборки мусора не достичь. При этом, в современных системах, даже если программисту кажется, что в его программах нет сборки мусора, на деле она есть на уровне операционной системы. Кроме того, чтобы обеспечивать безопасность и гибкое управление ресурсами приходится работать через виртуальную память, а это накладные расходы на каждую операцию чтения или записи. Повторюсь, на каждую операцию доступа в память накладные расходы 20%.
Поэтому сравнение языков со сборкой мусора и без сборки несколько нечестное в современной экосистеме. Языки со сборкой мусора работают поверх другой системы со сборкой мусора и виртуализацией, то есть, в них
задача решается двукратно. И задержки вполне могут быть результатом работы подсистемы виртуальной памяти, а не самого алгоритма сборки мусора. Тут конкретно всегда смотреть надо. Замусорить кэши трансляции до многократного падения производительности можно на любом языке программирования. Когда есть сборка мусора, автоматически, вина перекладывается на неё, но часто проблемы бывают вызваны корявыми структурами данных.
Честных сравнений я не видел. Возможно, прогресс с юниядрами (unikernel) покажет картину более отчётливо.
Довольно интересно тут пофантазировать на тему того, что где-то в недрах Google живут серверы, на которых работает только Go runtime, без прослойки обычной ОС, и за счёт этого они экономят 10% вычислительного времени.
4. Ещё одна проблема в языках программирования. Современное состояние искусства программирования подразумевает интенсивное использование указателей. С этой точки зрения Go намного лучше Java, потому что позволяет создавать массивы разнообразных структур. Меньше нагрузка и на кэши, и на сборщика мусора. В функциональных языках ситуация ещё лучше. Статический анализ убегания (escape-анализ) и оптимизации могут утрамбовывать данные для них очень плотно, даже до сворачивания динамических списков в массивы на стеке. Однократное присваивание существенно облегчает задачу.
К сожалению, пока мы не умеем писать системный код на функциональных языках. Но люди работают над этим.
5. В общем, всё непросто. Ну, и чем старше становишься, тем больше тянет на языки со сборкой мусора. Я думаю, это не от лени, а от того, что приходится решать всё более логически сложные задачи. На текущем уровне технологий, лично я стараюсь высокопроизводительный код делать максимально тупым в отношении управления ресурсами, выносить его в отдельные процессы, а управление этими тупыми процессами прописывать уже на языках со сборкой мусора. Ну, или же, если заказчик упёртый, то делать то же самое, но с аренами. А иначе сложную систему просто не сделать.
6. Будущее, скорее всего, за системами сборки, получающими информацию от статических анализаторов кода. В конце концов, всегда можно пофантазировать о сборщиках мусора, оптимизированных под семантику конкретных структур данных.asdf87
04.01.2017 15:15«Но дальнейшее перепроектирование и переписывание алгоритмов управления ресурсами даются непросто. Да и потом, нормальный статический анализ кода может все эти эффекты просто вычислить.»
Но для того чтобы программа оставалась корректной логику ее работы все равно приходится переделывать и не немного, а очень сильно вне зависимости от того используете ли вы GC и/или статический анализатор.
Вы не верите в дальнейшее развитие Rust на этом поприще только из-за сложности разработки на нем?
По идее, Rust очень молодой язык программирования с новыми/революционными концепциями (по крайней мере, для mainstream'a). Мне кажется, со временем эти новые концепции должны вполне нормально осесть в головах у разработчик + появятся специализированные для Rust'a tool'ы для разработки. Это должно уменьшить порог входа и упрость понимание и разработку программ на Rust. Как дополнительный бонус, выполнение программы становится более детерминированным и язык провоцирует писать корректные и оптимальные в плане используемых ресурсов программы.mikhanoid
04.01.2017 16:13Вы не верите в дальнейшее развитие Rust на этом поприще только из-за сложности разработки на нем?
О! Это интересный техно-культурно-экономический вопрос. Я не знаю, но у меня есть некоторые соображения, возникшие в результате наблюдений за lambda-the-ultimate и ICFP.
Алгоритмы компиляции (а именно, вывода типов) стали экспоненциально умнее, и железо стало экспоненциально быстрее. Оно вышло на какой-то предел одноядерной производительности сейчас, но появляются облака, GPU, FPGA и прочее. И в оптимизаторах начинают применять вычислительно тяжёлые методы. Z3, например, для решения SAT. Или даже машинное обучение в некоторых пока экзотических случаях.
И это помогает. Например, llvm-souper (супер-оптимизатор для llvm) помогает разработчикам искать новые варианты оптимизации кода, до которых они сами не додумались. Потом эти методы прописывают в llvm.
Видимо, в долгосрочных планах у многих разработчиков стоит задача о переносе компиляторов в облачную инфраструктуру, где за счёт эффектов масштаба возможны новые интересные вещи. Вот есть llvm-souper, есть MirageOS от сообщества OCaml.
И эти штуки неплохо работают. Особенно SAT-решатели на задачах типизации. То есть, сейчас относительно просто по коду программы присвоить тип переменной какой-нибудь такой: int 32 бита, у которого 3-ий бит всегда 0. То, что анализатор может вытащить из программы, программист, наверное, тоже может написать явно, но ему придётся писать это слишком долго.
И вот касательно Rust. Возникает тогда вопрос: а зачем разработчику прописывать все эти ограничения на переменные, если, всё равно, придёт мощный оптимизатор и сам всё это посчитает вместе с сотней других важных параметров?
Если бы Rust появился на 10 лет раньше, он бы успел набрать критическую массу пользователей и успел бы в mainstream. А сейчас, есть у меня подозрения, что JavaScript в недалёком будущем производительностью не будет уступать C++
https://github.com/ispras/llv8 (ispras — это http://ispras.ru)
Вот и возникнет у простого программиста вопрос: а зачем учить Rust, если можно выучить JavaScript и не особо проиграть?TargetSan
04.01.2017 16:58И вот касательно Rust. Возникает тогда вопрос: а зачем разработчику прописывать все эти ограничения на переменные, если, всё равно, придёт мощный оптимизатор и сам всё это посчитает вместе с сотней других важных параметров?
Я бы сказал — для самого разработчика. Это может не играть роли для монолитного бинарника. Но ох как важно для библиотечного кода. В том числе позволяет оптимизатору делить работу на куски поменьше. Без аннотаций придётся каждый раз делать анализ всего графа программы.
https://github.com/ispras/llv8 (ispras — это http://ispras.ru)
Это крайне интересный проект, без шуток.
Но как он отработает на "обычном" JavaScript с пачками интроспекции, манкипатчингом и другой динамикой? Как он будет жить на библиотечном коде? Проблема в динамической от природы и, скажем прямо, кривой от рождения семантике JavaScript. Даже самый умный оптимизатор будет вынужден будет думать о ситуации когда в некое под-дерево функций передадут совершенно не то, что ожидали.
mikhanoid
04.01.2017 17:02+1Так он же JIT. Когда есть вероятность, что передадут неожиданное, ставит проверки.
asdf87
04.01.2017 17:46«А сейчас, есть у меня подозрения, что JavaScript в недалёком будущем производительностью не будет уступать C++»
А чем это лучше java'ого jit'a? Кроме того что java — это статически типизированный язык, а javascript — динамический и llv8 еще сложнее это все будет правильно выводить и делать какие-нибудь предположения для серьезной оптимизации.
Что-то мне не верится, что даже джавовые БД-шки будут в обозримом будущем конкурировать по производительности с С/С++-ными, а тем более javascript'овыми.mikhanoid
04.01.2017 18:11Ничем не лучше. Да и Java не настолько уж статичен. Загрузка классов же. Но я к тому, что прогресс ощутимый и в Java тоже. Опять же, с базами данных тоже не понятно. Люди вон вообще базы используют на Erlang, потому что иначе проблему просто не решить. Си++ не позволяет так масштабироваться. Я думаю, там в прямом смысле дилемма: либо писать на Си++ 30 лет, либо писать на Erlang лет 5. И не потому что программисты плохие, а потому что вот так вот оно просто потому что объективно сложно.
lorc
04.01.2017 16:14Кроме того, чтобы обеспечивать безопасность и гибкое управление ресурсами приходится работать через виртуальную память, а это накладные расходы на каждую операцию чтения или записи. Повторюсь, на каждую операцию доступа в память накладные расходы 20%.
WAT? Вы уверены что на каждую операцию? Что-то мне подсказывает, что если нужные записи уже в TLB, а сами данные лежат в кеше, то overhead будет намного меньше.
Я ещё могу согласиться на 20% накладных расходов в среднем (да, page walk занимает много времени, не говоря о paging fault). Но если соблюдать локальность. то накладные расходы должно быть намного меньше.mikhanoid
04.01.2017 16:44TLB стоит на критическом пути при доступе к кэшу. А по виртуальной памяти адресоваться в многопроцессорных системах тяжело. Свой такт он забирает (опять же, это вопрос tradeoff-ов, можно сделать маленький, чтобы успевал проработать, но тогда часто придётся обновлять, текущее решение такое вот: довольно большой tlb, но отнимающий такт при доступе). Вообще, борьба за производительность TLB ведётся эпическая, особенно во встраиваемых микропроцессорах. Он же ещё электричество жрёт.
PsyHaSTe
09.01.2017 19:14В такие моменты становится жаль проекты вроде MS Singularity, с её абсолютно адресуемой памятью, где TLB можно было бы избежать в принципе…
khim
09.01.2017 20:01Эльбрус — тоже был из той же серии. Но — не выходит пока. И неясно — выйдет ли когда-либо…
mikhanoid
09.01.2017 21:52+1У Эльбруса другая идея. И тоже не особо аппаратно экономная: нужно память тратить на тэги. Если нужна надёжность, то это отлично. Но скорость снижает и сложность архитектуры увеличивает.
Но, я думаю, тут надо учитывать какие-нибудь экономические факторы. Singularity была бы настоящий disrupt-технологией. То есть, рынок железа существенно бы трансформировался. Может быть, поэтому решили не выпускать. А, может быть, просто Z3 был сырой, и не умел считать всё, что нужно.
Но сейчас всякая такая техника то там, то здесь вылазит. Например: Ur/Web. Штука умеет целиком про клиент-серверное приложение с базами данных доказывать, что оно корректное, вычислять регионы жизни ресурсов и обходится без сборки мусора. Скорость на уровне C++, а кода раз в 10 меньше. Плюс GADT и всё такое прочее. Минус в том, что не для каждой программы она способна построить вывод всего того, что нужно, иногда вывод отрезается по глубине рекурсии. Но автор пишет, что, для обычных программ, а не специальных контрпримеров, всё нормально.
khim
04.01.2017 16:45Когда есть сборка мусора, автоматически, вина перекладывается на неё, но часто проблемы бывают вызваны корявыми структурами данных.
Это спор между тупоконечниками и остроконечниками. Если у вас все структуры данных — хорошо «отстроены» и вы хорошо понимаете когда и как выделяется/освобождается память — то вам GC не нужен от слова «совсем».
А вот если код «писан индусами», не понимающими что, как и где вообще происходит (знаменитое «к пуговицам претенции есть?»), вот тогда — с GC всё будет тормозить и жрать память, как не в себя, а без GC — вообще развалится к чертям собачьим.
Так что хотя прямой связи между использользованием GC и тормозами — нет, но корелляция — почти 100%.mikhanoid
04.01.2017 16:52вы хорошо понимаете когда и как выделяется/освобождается память
Вот это не всегда возможно понять. И приходится в таких случаях писать свою версию GC. Ну да, мы это назовём частью структуры данных. Но, фактически, это будет сборщик мусора. Хочу ли я постоянно писать сборщики мусора?
Теперь уже не уверен.
А вот если код «писан индусами», не понимающими что, как и где вообще происходит (знаменитое «к пуговицам претенции есть?»), вот тогда — с GC всё будет тормозить и жрать память, как не в себя, а без GC — вообще развалится к чертям собачьим.
Так вот именно! Только проблема в том, что в 90% случаев, ты сам и являешься таким вот индусом. Потому что, срок сдачи проекта «вчера». А за другие сроки сдачи тебе не заплатят. КапитализЪм основной двигатель сборки мусора.
Поэтому я и говорю, что имеет смысл разделять код на мусорный и на ручной, который будет писаться на Си с минимумом управления ресурсами, но при этом работать быстро. А мусорный — ну фиг с ним. Главное, быстро нафигачить. К сожалению, такова реальность.khim
04.01.2017 17:00Хочу ли я постоянно писать сборщики мусора?
Ну зачем же каждый раз с нуля писать? Есть же готовые библиотеки.
Теперь уже не уверен.
Проблема в том, что альтернатива — это «GC как раковая опухоль». Всепроникающий монстр проникающий во всё в системе: от API до способов вызова функций и вообще всего, что есть в программе.
Поэтому я и говорю, что имеет смысл разделять код на мусорный и на ручной, который будет писаться на Си с минимумом управления ресурсами, но при этом работать быстро. А мусорный — ну фиг с ним. Главное, быстро нафигачить. К сожалению, такова реальность.
Совершенно верно. И с этой реальностью я даже спорить не хочу. Если ваша задача — что-то сделать быстро (и неважно что память будет жраться «как не в себя» и другие ресурсы будут транжириться тоже) — то GC вполне себе неплохое решение.
Но почему-то апологеты GC этого признавать не хотят и пытаются делать вид, что у «всеобъемлющего GC» есть какие-то другие преимущества. Нету. Всё остальное — проблемы. А важно ли вам конкретно это достоинство или нет — действительно зависит от задачи.mikhanoid
04.01.2017 17:19Ну зачем же каждый раз с нуля писать? Есть же готовые библиотеки.
Ага. Только я боюсь библиотек, в которых есть управление памятью. Собственно, отсюда Rust и начался, потому что Mozilla задолбалась бороться с утечками. Это серьёзная проблема, к сожалению. Тут надо как-то аккуратно совместно разрабатывать и семантику языка, и методы управления памятью, и методы управления управлением памятью. Потому что realtime и сборка мусора не совместимы по определению на уровне современных технологий.
А хотелось бы. IMHO, было бы прекрасно, если бы на уровень языка получилось бы как-то аккуратно вынести то, что делает UNIX, управляя процессами.
Idot
05.01.2017 12:19+1проблема в том, что в 90% случаев, ты сам и являешься таким вот индусом. Потому что, срок сдачи проекта «вчера». А за другие сроки сдачи тебе не заплатят. КапитализЪм основной двигатель сборки мусора.
Delphi — прекрасно обходится без сборка мусора, потому «Лучший сборщик мусора это тот который не нужен» googol
И Delphi вполне упешно справляется с задачей быстрого создания работающего кода, потому что он оптимизирован под RAD = Rapid Application Development = Быстрое Создание Приложений.Maccimo
07.01.2017 07:37+2То, насколько успешно справляется с задачами Delphi можно оценить, сравнив количество Delphi-вакансий с количеством вакансий по той же Java.
Idot
07.01.2017 15:46+1Это совсем по другой причине — не потому что в Delphi нет сборщика мусора и он в нём «остро нуждается», а потому что Java работает и под Web тоже, а на Delphi — только толстый клиент.
konsoletyper
05.01.2017 13:35А вот если код «писан индусами», не понимающими что, как и где вообще происходит (знаменитое «к пуговицам претенции есть?»), вот тогда — с GC всё будет тормозить и жрать память, как не в себя, а без GC — вообще развалится к чертям собачьим.
Есть одна контора, где код, видимо, писан индусами (хотя почему-то основной офис у них в Санкт-Петербурге). Только вопрос: а есть ли в природе хоть одна такая же крутая IDE, написанная крутыми мужиками на C++? Ах да, IDE крутым парням не нужны, vim хватает всем, а IDE — это костыль для индусов, которые не понимают, что они пишут.
mikhanoid
05.01.2017 13:55+1Крутые IDE, написанная на Си (я думаю, что нам тут не важно, на Objective или Plus Plus) — это, например, Microsoft Visual Studio, XCode или Open Watcom. ViM и Emacs, конечно, системы с другим подходом к организации проесса написания кода, но я бы не сказал, что они менее эффективны, чем IDE. А почему? А потому что, сюрприз: ViM и Emacs не на Си написаны, на Си написаны только их ядра, а основной функционал прописывается в скриптах (со сборкой мусора), которые дают намного-намного больше возможностей, чем типичная IDE. То есть, как бы, Вы мимо злорадствуете
0xd34df00d
04.01.2017 18:36В функциональных языках ситуация ещё лучше. Статический анализ убегания (escape-анализ) и оптимизации могут утрамбовывать данные для них очень плотно, даже до сворачивания динамических списков в массивы на стеке. Однократное присваивание существенно облегчает задачу.
При этом GC в де-факто стандартной реализации нежно любимого мной хаскеля (ghc) совершенно не масштабируется на многопоточные приложения, по моему опыту.
К сожалению, пока мы не умеем писать системный код на функциональных языках. Но люди работают над этим.
Так низкоуровневый системный код и на языках с GC вроде не особо пишется.
mikhanoid
04.01.2017 19:07Так низкоуровневый системный код и на языках с GC вроде не особо пишется.
Это интересный вопрос, на самом деле. Была, например, jNode. Там управление памятью прописано на Java, и пользователь мог даже свои писать сборщики. Конечно, в регистры процессора всё писалось при помощи ассемблера.
Как-то вот так оно. Не очевидно.0xd34df00d
04.01.2017 20:11«Была» — ключевое слово.
Так-то и Singularity от MS была, и на Lua под микроконтроллеры писать можно.
mikhanoid
04.01.2017 20:33Ключевое слово «можно». Идея живёт, вроде как. Не отпускает людей память о lisp-машинах.
khim
04.01.2017 21:16+1Архитектурным астронавтам и не такое дано придумать.
Но на практике самое большое достижение — это уничтожение одной крупной компании (Sun), потеря миллиардов долларов и полнейшее желания у разработчиков отказываться от их любимого BLAS.
Ну вот не хотят — и всё тут. Ни одна система без поддержки нативного кода долго не продержалась: либо он добавлялся (Android, Windows Phone), либо платформа умирала (Blackberry, Java ME). Единственным исключением явился Web, но он потихоньку подходит к тому, что как о платформе для разработки приложений о нём скоро забудут (хотя блоги, конечно, никуда не денутся).
А уж когда в системе оказывается несколько «главных» (какой-нибудь Unity на Android'е) — то тут смело можно забывать и о надёжности и о безглючности (которые, как бы, апологеты GC в первую очередь обещают).mikhanoid
04.01.2017 22:03-1Апологеты GC не обещают надёжность и безглючность. Правильная формула такая: если нам необходимы надёжность, выразительность, и безглючность, то GC — это необходимый компонент системы. А дальше уже вопрос реализации и дизайна самого языка.
Java вот совершенно не разрабатывалась для вычислительных задач. Но тем не менее, прогресс в скорости исполнения кода в Java-экосистеме впечатляющий.
Но есть языки, которые специально затачивают пот вычисления. Вот, например, сравнение от IBM.
Go, OCaml и Haskell, кстати, генерируют native-код и вполне способны работать с BLAS.
А то, что у нас куча legacy-библиотек, учебников, дао в рабочих коллективах и прочего, что приводит к жизнеспособности языков, подобных Си++. Ну, да. Есть такое. Но это не техническая проблема. Культурная, скорее. Это тоже важно, конечно.0xd34df00d
05.01.2017 07:08Go, OCaml и Haskell, кстати, генерируют native-код и вполне способны работать с BLAS.
Ну естественно, если FFI в С есть. Правда, непонятно, причём тут возможность использования BLAS.
Haskellghc генерирует нативный код, конечно, но рантайм там жирноват, мягко скажем, не сишечный и даже не плюсовый, поэтому называть это нативным языком несколько странно. А так и JIT в итоге нативный код генерирует, но это же не повод.
Вообще, представляется немного некорректным использование термина «нативный» в данном контексте. Не нативный, а достаточно близкий к железу, что ли.
А то, что у нас куча legacy-библиотек, учебников, дао в рабочих коллективах и прочего, что приводит к жизнеспособности языков, подобных Си++.
Да хороший язык, вполне себе, если писать уметь, конечно, но это для любого языка верно. Я вообще умудряюсь в ряду самых любимых держать плюсы и хаскель.
С другой стороны, вот приду я в условную новую компанию, пусть даже очень большую, а там на какие-нибудь 10к разработчиков хаскель знают хорошо если 50 человек. Тимлиду моему хаскель внедрять будет рискованно, непосредственным коллегам он тоже не сдался, им не пло?тют за изучение, дома семья-дети, какой хаскель ещё, да и вообще на плюсах неплохо кормят. И что делать?
mikhanoid
05.01.2017 13:32Haskell ghc генерирует нативный код, конечно, но рантайм там жирноват, мягко скажем, не сишечный и даже не плюсовый, поэтому называть это нативным языком несколько странно. А так и JIT в итоге нативный код генерирует, но это же не повод.
Среда для Си, как минимум, включает в себя операционку и некоторое количество защитного и отладочного железа в процессоре. Если мы хотим нечто минимально надёжное, то хотя бы регионы памяти надо поддерживать. А если мы хотим программу хотя бы на 1000 строк кода, то, как минимум, нужен JTAG. То есть, даже в low end микроконтроллерах не всё так просто.
Runtime для Haskell тоже штука такая. Не исследованная. Никто же не пытался его максимально облегчить и оставить минимум необходимого. Насколько мне известно. О! Вспомнил. На самом деле, было такое. Проект House. Они запили полноценную операционку на 1.44 дискету. Микроядро там, конечно, L4 на Си. Но всё же.
Я не говорю, что лучше, а что хуже. Но на картину надо смотреть в целом, с учётом аппаратуры и операционной системы.
На самом деле, с Си ситуация довольно тяжёлая. Например, процессор вынужден постонно обеспечивать видимость строгого порядка доступа к памяти, потому что указатели. И если не обеспечивает, то приходится постоянно вставлять барьеры. А для Haskell такого не надо. И, значит, если у нас Haskell-экосистема, то мы можем позволить себе 16-ти ядерный процессор по цене 4-ёх ядерного, потому что можем транзисторы на ALU потратить, а не на MOB.
И вот, как бы, сиди и думай над tradeoff-ами. Тут можно 10N диссертаций точно влёгкую написать.
Да хороший язык, вполне себе, если писать уметь, конечно, но это для любого языка верно. Я вообще умудряюсь в ряду самых любимых держать плюсы и хаскель.
С другой стороны, вот приду я в условную новую компанию, пусть даже очень большую, а там на какие-нибудь 10к разработчиков хаскель знают хорошо если 50 человек. Тимлиду моему хаскель внедрять будет рискованно, непосредственным коллегам он тоже не сдался, им не пло?тют за изучение, дома семья-дети, какой хаскель ещё, да и вообще на плюсах неплохо кормят. И что делать?
Так я и не утверждаю, что Си++ плохой. Я говорю, что он жизнеспособный, а это даже важнее, чем хороший. Но вопрос в том, чем поддерживается жизнеспособность? Обычно утверждается, что причина в скорости получаемого кода. Я же просто осторожно и, надеюсь, обоснованно сомневаюсь в этом утверждении.
Вот в том же сценарии, когда у нас 10k программистов пишут на Си++, очень даже может быть, что на каком-нибудь Stalin Scheme они бы получали более эффективный код. Scheme — это динамический язык вообще; но картинка такая вот, тем не менее, в которой нужно учитывать, что GCC пишет 100 человек, а Stalin был написан одним профессором.
Почему бы Stalin мог для них быть лучше? Потому что, все же понимают, что 10k программистов пишут, в основном, г-код. А особенность Stalin-а в том, что он процессе компиляции абстрактно исполняет программу, и процентов 50 г-кода посто выкидывает, заменяя константами.
Но Scheme или Haskell вряд ли в такую компанию можно будет внедрить, у ней слишком большая масса, инерция и объёмы legacy-кода. Тем более, что Haskell может и не давать никаких profit-ов именно тем приложениям, которые в этой компании пишут, а осваивать его тяжело. Си++ же не такой уж и плохой язык, и он работает.
Но а зачем в такую компанию внедрять Haskell? Я думаю, такие компании сами отвалятся. Если рядом будет компания, в которой 5k разработчиков, которые на Haskell делают то же самое, но в 2 раза дешевле, в 8 раз безглючнее, и работать оно будет в 64 раза быстрее, потому что на GPU хорошо уложится, то экономика сделает своё дело.
В финансовых секторах Haskell быстрыми темпами распространяется, потому что люди готовы отчаянно бороться против самострела в ноги, и «в 8 раз безглючнее» их мотивирует.
Или вот ещё. Физики всегда любили Fortran. И до сих пор его нежно любят. Но постепенно все переходят на Python. Потому что profit: можно писать программы в 16 раза быстрее, чем на Fortran, а работают они примерно с той же скоростью, или даже в 100 раз быстрее, если попадают на GPU. И, кстати, так получается не только, потому что можно лего использовать библиотеки, которые уже были написаны на Fortran, но и потому что у Python хорошо подходящая для оптимизаций арифметическая семантика, и PyPy способен на чудеса (есть проект Pixie, который отдаёт PyPy абсолютно адский г-код, но JIT его вытягивает на отличную скорость).
Как-то вот так вот оно сейчас неоднозначно. Вот 15 лет назад было хорошо. Хочешь скорости — пиши на Си или Фортран, и не заморачивайся. А сейчас нельзя не заморачиваться. Потому что слева сидит Василий, который ту же программу напишет на Python, в скорости проиграет процентов 15, но на рынок выйдет в 5 раз быстрее. А справа сидит Пётр, который ту же программу будет писать на Haskell, времени затратит, наверное, столько же, сколько и я, но он её ещё проверит при помощи Liquid, и ему не придётся потом полгода исправлять баги в production. И да, коды Васи и Пети тоже будут отлично работать на микроконтроллерах.
И что вот с этим делать?
lany
04.01.2017 14:55+4Сборка мусора уже решила подавляющее большинство проблем. Сложности с ней возникают только в определённых областях, где особые требования по отзывчивости или расходу памяти, например. Для огромного пласта приложений проблема давно решена и не стоит.
Комментарий похож на нытьё в духе "вам не кажется, что медицина идёт не туда? Столько лет что-то исследуют, мышей травят, новые какие-то лекарства изобретают, говорят, что вот-вот будет хорошо, а люди как болели, так и болеют". На самом-то деле успехи громадны и многие смертельные заболевания успешно научились лечить, но, конечно, это не мешает поныть.
chabapok
05.01.2017 04:16Вопрос очевидный, и ответ очевидный — проблем создает гораздо меньше. Аналогичная задача с подходом delete-delete[]-free привела бы к гораздо большей фрагментации памяти — и все упало бы с OOME. Просто, сишники сказали бы «нам мало памяти», и это было бы неоспоримо. Джависты в этом случае бегут тюнить жс только потому, что сан придумало full gc, который позволяет «собрать пенки» уплотняя дырки. На самом деле, если в нужный момент принять, что при данном размере хипа оно давно уже должно oom-нуться — но вопреки этому просто подтормаживает, то все может стать проще.
Есть ведь определенная зависимость необходимого размера хипа от размеров данных и порядка аллокации. Если ее не соблюсти — задача оазмещения данных так или иначе превращается в «впихнуть невпихуемое»0xd34df00d
05.01.2017 07:09Аналогичная задача с подходом delete-delete[]-free привела бы к гораздо большей фрагментации памяти — и все упало бы с OOME.
А почему мы ограничены 32-битным адресным пространством?
Не путайте размер хипа и размер адресного пространства.
mikhanoid
05.01.2017 13:40Это если объекты большие. А у серверных ребят наоборот всё. У них там куча мелких записей и сложные графы зависимостей между ними. Ну, вот да, стиль программирования такой. То есть, вполне бывает так, что одна запись о ФИО держит в памяти страницу целиком.
chabapok
05.01.2017 16:01+1А где я писал, что мы ограничены? Я не понял вашу мысль, можете подробней обозначить, куда вы клоните?
Тут важно, не чем конкретно мы ограничены, а что ограничение вообще есть — и оно меньше, чем то, о котором мы всегда мечтаем. Если для решения задачи надо N обьектов — и полюбому надо где-то разместить. И если не хватает памяти — придется их как-то скидывать на диск, уплотнять и тд. Это займет время. Решение — или переписать задачу, чтоб N уменьшилось, или как-то более умно уплотнять объекты, или добавить памяти чтобы все поместилось. Сишники добавляют память или переписывают задачу. Джависты в первую очередь думают, как более умно уплотнять — и при этом над ними ржут сишники. Но при этом джависту доступны все 3 варианта, а сишнику доступно только 2. Если у сишника хип фрагментирован — он никогда не признает, что «а вот java мне б его уплотнила и все работало», он догматично заявит — мало памяти, и никакие аргументы в стиле «ты же сишник, ты можешь все, пойди и отфрагментируй» его не убедят а только разозлят.
Ogoun
04.01.2017 07:27+1Задумывался уже давно, есть ли сборщики работающие на событиях? Чтобы алгоритм был наподобие, — количество ссылок на объект стало равным нулю (количество ссылок считать без построения полного графа, а путем аспектных вставок компилятором, на границы выхода из областей видимости, например), в этом момент для сборщика генерируется событие что объект освободился, и он перераспределяет его память в фоне, работая при этом с равномерной нагрузкой в отдельном потоке. И чем был бы плох такой вариант? Как минимум отпадет проблема пауз.
xhumanoid
04.01.2017 08:43+4давно существуют алгоритмы и сборщики на подсчете ссылок, но у них есть свои проблемы:
1) атомарное увеличение-уменьшение количества ссылок не самая быстрая операция, а там нужна именно атомарная, так как часто мы не знаем куда убежала ссылка на объект и какие потоки еще держат данный объект
2) полностью вопроса с паузами он не снимает, хотя и делает на порядки более предсказуемым поведение.
Про второй пункт более подробно:
представьте что у Вас имеется структура в виде дерева и в программе держится только корень,
в какой-то момент корень выходит из зоны видимости и следовательно может быть собран,
после того как собран корень уменьшаются счетчики на объекты которые держались корнем и они тоже отправляются на удаление
и так проходит по всем элементам
в итоге у Вас возникает проблема, что выход из блока и инвалидация всего одной ссылки вызывает каскадную нагрузку на работу с памятью и очень часто предсказать Вы не можете в каком месте это выстрелит.
в классических языках с поколениями пройдет фаза mark и потом copy в новую область все кто выжил, а не будет вызываться пачка мелких удалений, просто пометим всю область как мусор и занулим.
p.s. известный факт: иногда из программы проще выйти по exit сразу в операционную систему, чем аккуратно освободить все участки памяти по очереди =) это касается любого managed языкаlorc
04.01.2017 16:19Да и не только managed. Освобождение всех объектов ядра (открытые файлы, сокеты, мютексы, shared memory), возврат страниц ядру — это syscall на каждый такой объект, а значит переключение контекста туда-обратно. Лучше один раз вызвать exit() и пусть ядро в своем контексте освободит всё это скопом.
akamensky
04.01.2017 09:13Проблема пауз не отпадет. Она просто кардинально изменится. События не означают отсутсвие пауз, они означают что на каждый освободившийся объект будет приходится своя отдельная [очень маленькая] пауза.
khim
04.01.2017 16:50Позравляю. Вы только что изобрели новейшую и секретнейшую технологию. Которая столь нова, что статья в Wikipedia даже пятилетний юбилей не отметила ещё.
naething
04.01.2017 09:58-1Сделали бы уже нормальный системный язык наконец:
— с ручным управлением памятью и детерминированными деструкторами;
— бритвенно острый, как C++;
— но с нормальным поведением по умолчанию, чтобы было меньше способов порезаться;
— с современной системой типов как в Rust.
Чувствую, не дождусь. Придется самому сделать, когда выйду на пенсию.senia
04.01.2017 11:03+3— с современной системой типов как в Rust.
Почему «как»? Чем Rust не устраивает?
Деструктор детерминирован, способы порезаться сведены к минимуму, но есть unsafe.mikhanoid
04.01.2017 11:54-1Rust громоздкий. Да и система типов в нём не такая уж современная. Современные системы типов, наоборот, стараются свести к минимуму аннотации, требующиеся от программиста. Вроде, линейные и подстркутурные типы можно выводить автоматически, не понятно, зачем в Rust это существенно перекладывается на программиста.
splav_asv
04.01.2017 12:42А можно, если не затруднит, пример где Rust требует лишних аннотаций и тот же кусок на языке с более современной системой типов?
mikhanoid
04.01.2017 13:25Ну, вот, например:
http://benchmarksgame.alioth.debian.org/u64q/program.php?test=nbody&lang=ocaml&id=1
http://benchmarksgame.alioth.debian.org/u64q/program.php?test=nbody&lang=rust&id=2
В большинстве случаев в этих тестах benchmarksgame код на Rust и Ocaml работают с примерно одинаковой скоростью. В некоторых случаях Ocaml сильно проигрывает. Но тут не понятно, за счёт чего. Скорее всего, у них оптимизатор для x86_64 не такой хороший, каким он мог бы бытьTargetSan
04.01.2017 14:01Я бы сказал что код на Rust процентов на 50 не-идиоматичный. Чего только стоит клон списка тел при подсчёте попарных разниц.
mikhanoid
04.01.2017 14:28Ну, насколько я понимаю, они показывают самую быструю версию кода из тех, что есть. В конце url id=1 можно поменять и посмотреть другие версии.
TargetSan
04.01.2017 14:08+1В целом же не вижу особой "перегруженности". Да, OCaml значительно короче — но это объясняется просто другими решениями при дизайне языка. В частности, module-wide type inference. В Rust сознательно ограничили вывод типов телом функции. Ну и по мелочи различия — например в OCaml версии нет вектора как отдельного типа, а в Rust — есть.
mikhanoid
04.01.2017 14:46Дело не в длине текста, а в том, что надо постоянно писать &[], или &mut &'a [T]. Вопрос в том, а зачем? Ведь, есть же алгоритмы вывода типов (и не только типов, много чего можно выводить, начиная с условий монотонности и конечности рекурсий и заканчивая теми же ограничениями на времена жизни переменных; можно даже ранги массивов автоматически подсчитывать). Современный уровень этой технологии позволяет ограничится описанием типа модуля. Остальное можно вывести автоматически. Так зачем заставлять человека делать работу машины?
Код понятнее от этого, вроде, не становится. А проблема Rust, как мне кажется, ещё и в том, что требуется довольно много boilerplate-кода писать для изменения типов значений.
То есть, вместо того, чтобы думать об алгоритме, который сам по себе сложный, я ещё должен думать о том, как это всё размешается по памяти и явно это прописывать. А алгоритм ещё и работать должен на какой-нибудь комбинации GPU в кластере.
Вот решение той же задачи на SAC: http://www.sac-home.org/doku.php
Вот таким, imho, должен быть идеальный язык программирования. Остальное система должна вывести сама. Сражу скажу, что не считаю SAC идеальным языком. Всё же это функциональный язык для вычислений, ядро операционной системы на нём не написать. Но он быстрый, и куда-то в этом направлении надо идти, мне кажется.snuk182
04.01.2017 15:23Просто то, что обычно называется «багфиксы» и делается после альфа-релиза, в Rust требуется учитывать изначально, оттого и код «громоздкий». Можно было бы в теории еще больше, чем сейчас, статически анализировать код при компиляции и автоматически подставлять `mut` там, где значение переменной меняется, но во-первых, компилятору не все такие моменты могут быть очевидны, а во вторых, разбор ошибок таких случаев будет совершенно нетривиальной задачей, а на Rust и так жалуются (совершенно незаслуженно), что язык слишком сложный.
asdf87
04.01.2017 15:47«Современный уровень этой технологии позволяет ограничится описанием типа модуля. Остальное можно вывести автоматически»
Вас не пугает то количество «магии», которое будет «под капотом» у этого компилятора? Тут я имею в виду даже не столько сложность самого компилятора или его время работы, а то что, если компилятор (или программист) сделает/напишет не так и конечная программа будет работать медленно или не правильно, то какому богу после этого молиться? Ведь надо будет всю эту компиляторную магию выполнить в голове, потому что вменяемую и достаточно быстро работающую IDE-шку для такого сложного языка вряд ли получится сделать.mikhanoid
04.01.2017 16:29Живут же люди с современными компиляторами, которые довольно часто делают что-то не так и неправильно. В современных компиляторах уже довольно много магии и багов.
Тут, наоборот, если компилятор руководствуется формальным выводом типов, а не какими-то неформальными ad-hoc методами, он может дерево выводов в какой-то форме показать и объяснить программисту, почему кусок ассемблера именно такой. В Guile сейчас такое пытаются сделать. Довольно удобно, надо сказать: пишешь функцию, а среда позволяет посмотреть, во что она превращается на разных стадиях оптимизации.
Ну, понимаете, тут же tradeoff простой: либо мы пишем мало кода и редко попадаем в плохие ситуации (ну, потому что, большинство строк кода не такие уж и сложные), либо мы ежедневно пашем как папы Карлы, чтобы получить небольшую delta в функционале.
Release fast, к сожалению, экономически более выгоден. Можно боятся, но машины победили. Это факт. Пора приспосабливаться.asdf87
04.01.2017 18:34В том то и дело, что написать код совсем не проблема (IDE с автокомплишеном, сниппетами и прочими плюшками очень в этом помогают). А вот разбираться в коде где у тебя «голые» вызовы функций без явного указания их ограничений — это геморой.
mikhanoid
04.01.2017 18:57А я и не говорю, что написать код проблема. Я говорю, что изменить код проблема. И проблема даже не в том, чтобы аннотации написать, проблема в том, чтобы изменить их схему, придумать новую, которая бы вписалась в старый код, и в новый. Собственно, набор буковок — не проблема.
IDE или REPL-ы вполне помогают разобраться с автоматическими типами функций.asdf87
04.01.2017 19:15Так и изменить саму схему тоже не проблема. А вот потом поддерживать ее использование в актуальном состоянии статическая система типов поможет, а динамическая нет. Почему? Просто потому что у нее нет этой информации.
mikhanoid
04.01.2017 19:51Информации, на самом деле, очень много. Вопрос в том, как программист может зафиксировать интерфейсы и что он должен в этих интерфейсах фиксировать. Ну, и да, я не особый фанат динамических языков. Но и прописывать везде типы тоже не особо хочется. Даже если они абстрактные.
Тут дело не в том, что тяжело кнопки давить. А в том, что хочется как можно быстрее переписывать программы. То есть, схема типов важна где-то на уровне интерфейса, а то, как я внутри это всё фигачу должно просто сойтись к этому интерфейсу. Но я хочу свободу в процессе «фигак-фигак и в продакшн» внутри модуля.
Я бы в этом смысле Haskell бы считал идеальным, если бы мог читать его код спокойно.
0xd34df00d
04.01.2017 19:24Вас не пугает то количество «магии», которое будет «под капотом» у этого компилятора?
В алгоритме Хиндли-Милнера нет ничего магического.
а то что, если компилятор (или программист) сделает/напишет не так и конечная программа будет работать медленно или не правильно
Надо различать false positive и false negative. Добиться принятия некорректной с точки зрения системы типов программы существенно сложнее, чем непринятия корректной семантически. Что, в общем-то, хорошо.
Ведь надо будет всю эту компиляторную магию выполнить в голове
Не надо, Х-М достаточно интуитивен, особенно на идиоматичном окамлевском уровне, без наркомании с мультипараметрическими типами классов, data kinds, семействами типов или, упаси б-же, зависимыми типами.
asdf87
04.01.2017 19:42Х-М условия монтонности, конечности рекурсии и лайфтаймы тоже умеет выводить? Я имменно это «магией» называл.
Хотя вывод типов даже для приватных функций на уровне модуля, по-моему, перебор. Это действительно усложняет и замедляет понимание что происходит внутри функции.mikhanoid
04.01.2017 20:10Умеет. ХМ — это схема для вычисления типов по правилам типизации. Какие правила будут заданы, то он и выведет. Главное, чтобы правила были непротиворечивыми. Времена жизни в Rust тоже ХМ выводит.
Не знаю. У меня такой опыт: я легко читаю библиотеки на Си (где, в общем-то тоже не шибко круто с типами) или Scheme, но мне очень тяжело читать Haskell или шаблоны в Си++. Типовые переменные увеличивают сложность и объём внимания, который надо уделять коду.0xd34df00d
04.01.2017 20:18но мне очень тяжело читать Haskell или шаблоны в Си++. Типовые переменные увеличивают сложность и объём внимания, который надо уделять коду.
Они же в хаскеле не обязательны, а топ-левел-аннотации помогают быстрее найти нужную функцию и вообще понять, что к чему.
0xd34df00d
04.01.2017 20:13конечности рекурсии
Я бы посмотрел на систему типов с такими аннотациями.
лайфтаймы
Да.
А ещё есть регионы, rank-2 polymorphism для escapable-монадок и прочие замечательные вещи.
Хотя вывод типов даже для приватных функций на уровне модуля, по-моему, перебор. Это действительно усложняет и замедляет понимание что происходит внутри функции.
Коммитить топ-левел функции без аннотаций я не люблю, да. Но тайпчекер зачастую упрощает их написание: можно сначала написать только тело или оставить пару type holes, и потом по мере реализации их дописать.
lany
04.01.2017 15:02Конкретно этот тест тупой. Тут всё просто — кто смог заюзать векторные инструкции вроде sse3, у того 10 секунд, кто не смог, у того 20 секунд. В языках, которые не сводятся к примитивной математике, больше. Тут никакого мусора нет и он ни на что не влияет. И вообще как-то стыдно уже на benchmarksgame ссылаться-то...
mikhanoid
04.01.2017 16:32Мы не скоростью мерялись. А количеством аннотаций в коде. Я просто не знаю других источников, где одно и то же нетривиальное было бы написано на разных языках программирования. Если Вы о таком знаете, был бы признателен за указание (мне для работы надо).
Есть, конечно, ещё коды для курса AIMA, но там Rust-реализаций нет.
splav_asv
04.01.2017 16:51Код на Rust слегка странно выглядит, но это мелочи.
На сколько я знаю, у них с ML общий принцип вывода типов. И система типов Rust потомок системы типов ML.
Вывод типов ограничен сознательно, чтобы в элементарном куске кода была аннотация, сводит необходимость зря ломать голову к минимуму.mikhanoid
04.01.2017 17:11Ну, вывод типов — вообще штука стандартная. Вопрос только в том, какие именно типы выводить. И вот эти явные ограничения на времена жизни переменных — жестокая головная боль. В итоге, либо по 10 раз всё переписываешь, либо забиваешь и пишешь везде копирования. Но, блин, с этим и компилятор бы справился. Если бы они оставили аннотации только для модулей (что, вполне оправдано), было бы намного-намного проще. А для каждой функции — это перебор.
TargetSan
04.01.2017 17:25+1Работа над теми же non-scoped lifetimes ведётся. Не всё сразу. В конце концов, здесь есть возможность ослабить ограничения. Сильно подозреваю что и вывод типов на уровне модуля со временем завезут.
splav_asv
04.01.2017 17:33+1Спорить не буду, но сам считаю количество аннотаций в Rust приемлемым. Правда на *ML ничего даже среднего размера не писал. Видится мне в этом дело вкуса и, возможно, привычки.
Но когда читаешь тот же Servo (слегка ковырял ради интереса) если бы у функций не стояли параметры, стало бы оочень тяжело понять что происходит. Ну или вводить какие-то костыли с именованием функций дополнительные. А для маленьких задач — да раздражает слегка. Вероятно, для менее запутанных вещей уровень модуля подходит лучше.
senia
04.01.2017 21:59+1В той же Java спорят про введение val/var — не ухудшит ли это читаемость.
В C# использование var некоторыми гайдами не приветствуется (видел команды, где var не пропускали на review).
В scala, хоть она и позволяет выводить тип для нерекурсивных методов, стандартный de facto статический анализатор заставляет писать типы всех публичных методов явно.
Так что сам по себе вывод типов не всегда и не всеми рассматривается как безусловное благо.
Громоздкости в Rust я не заметил — для своих целей он требует минимум лишних приседаний. Правда изредка встречается некоторая сыроватость, связанная с временем жизни в некоторых случаях, например при обходе мутабельных коллекций. Но это исправляют постепенно.
По мне так к недостаткам Rust можно отнести слабый механизм макросов, основанный на тексте, а не на AST, но это я скалой ударенный.snuk182
05.01.2017 15:39В следующей версии обещают `proc_macros`, там все честно.
TargetSan
05.01.2017 16:12Не совсем. ЕМНИП Матсакис писал, что компилятор тоже будет отдавать поток токенов — но будет libmacro которая будет делать юзабельный АСТ и всякие ништяки. Я так понял, своего рода forward compatibility.
naething
07.01.2017 10:10В Rust прежде всего напрягает паранойя компилятора и постоянная возня с лайфтаймами, а также отсутсвтие исключений.
Из-за первого, например, на Rust нельзя реализовать двусвязный список без использования unsafe или лишнего оверхеда. Ну или бинарное дерево, где узлы хранят сслыки на родителей. Еще в Rust из-за требований к безопасности памяти необходимо проверять гранциы массивов в рантайме при обращении по индексу. В системном языке все это напрягает.
Я готов пожертвовать 100%-й безопасностью памяти в обмен на отсутствие лишней возни и оверхеда. То есть в моем списке требований расту не хватает той самой бритвенной остроты. На том же C++ можно писать безопасный код довольно просто, соблюдая определенную дисциплину. Только хочется иметь «safe defaults». Например, убрать из языка оператор new (за исключением placement new), конструктор копирования по умолчанию и т.п.senia
07.01.2017 10:49Паранойя компилятора в большинстве случаев оправдана. Единственная альтернатива «возне с лайфтаймами» — отслеживание всех тех же ограничений вручную. Либо на уровне документации, либо, что хуже, неявно. По исключениям тут спорить мне сложно: я предпочитаю FP подход, как в Rust.
Такие список и дерево и так не реализовать без Rc с Weak или ручного управления памятью (unsafe). Вообще не понятны ваши претензии к unsafe: вы пытаетесь сделать что-то, корректность чего, очевидно, нельзя проверить автоматически. Почему вы в этом случае не хотите указать явно, что вы понимаете последствия?
Без проверок границ — get_unchecked.naething
08.01.2017 01:24А мне не понятно, где вы увидели претензии к unsafe. Я вообще считаю, что Rust отлично спроектирован для поставленных целей, просто мне не вполне близки эти самые цели, поставленные разработчиками языка.
Я лишь хотел сказать, что для каких-то нетривиальных вещей зачастую приходится прибегать к unsafe. А на системном языке писать нетривиальные вещи как раз приходится часто. Получается, что основное заявленное достоинство языка — безопасность памяти — на практике не особо работает.senia
08.01.2017 01:31+1Если вы не можете разделить свой код на небольшую часть, работающую с unsafe и большую безопасную часть, то мне крайне сложно представить что же вы такое пишете.
naething
09.01.2017 04:09Хитрые структуры данных с нетривиальными зависимостями, не укладывающимися в концепцию scoped-based ownership. Например, для реализации текстового редактора могут понадобиться:
— список блоков текста, представленных указателями на внешние буферы;
— деревья типа rope/cord для быстрого поиска по номеру строки;
— индексы для подсветки синтаксиса и т.п.;
— список истории undo, также хранящий указатели на текст во внешних буферах;
— и так далее.
Все это может быть переплетено между собой и хранить ссылки друг на друга.
asdf87
07.01.2017 16:29+1«Я готов пожертвовать 100%-й безопасностью памяти в обмен на отсутствие лишней возни и оверхеда.»
Я надеюсь, что большинства программистов все же более прагматичный подход. Помимо safe-defaults хорошо бы иметь safe все остальное (на сколько это возможно), а в месте выбранного trade-off'а указать более явно какое решение принято, а в идеале еще и что послужило причиной такого выбора (но кто пишет коменты? правда?).
Вполне возможно, что вы хотите себя чувствовать джедаем и это просто не вашпутьязык, а другие хотят меньше задумываться о железе, а больше решать проблемы которые перед ними стоят, но при этом все-таки писать качественный стабильный софт. Если большую часть рутинной работы по проверке можно переложить на машины, почему бы не сделать это?naething
08.01.2017 01:05-1Я надеюсь, что большинства программистов все же более прагматичный подход. Помимо safe-defaults хорошо бы иметь safe все остальное (на сколько это возможно)
… меньше задумываться о железе, а больше решать проблемы которые перед ними стоят
Для этого есть Java — оличный язык.
Я не хочу чувствовать себя джедаем, просто когда я пишу на системном языке, мне не хочется, чтобы компилятор вставлял мне лишние палки в колеса. Параноидальная проверка лайфтаймов все равно не обеспечит 100%-й безопасности, но значительно усложнит процесс написания и модификации кода. По-моему, мой подход весьма прагматичен. Я просто хочу сделать свою работу, а не ковыряться со всякими<'a>,
просто чтобы доказать компилятору, что и так вполне очевидно программисту.asdf87
08.01.2017 14:09+3«Я просто хочу сделать свою работу, а не ковыряться со всякими...»
Так в этом изначальный посыл и идея языка заключается, что С++ тоже хотели сделать более безопасным и удобным, чем С. Придумали кучу «полезняшек»: 4 типа кастов, шаблоны, множественное наследование и т. п. А в итоге получился огромный монстр для которого во многих проектах от большей части функциона просто отказываются конвенциями.
Вот так и выходит, что просто сделать свою работу на системном языке не получается…
Java тоже не такой уж прям «отличный язык» в своей среде. Иначе бы не появилось куча других языков java-заменителей типа scala, ceylon, kotlin и т. д. Которые тоже статически типизированные, тоже под jvm, но только лучше чем java.
TargetSan
08.01.2017 23:08Вы видимо никогда случайно не добавляли элементы в вектор, по которому в это время итерировались. И не забывали инициализировать unique_ptr. И не делали случайное обращение к шареному ресурсу без синхронизации.
naething
09.01.2017 03:50Вы видимо никогда случайно не добавляли элементы в вектор, по которому в это время итерировались.
Было дело в молодости. Думаю, подобные проблемы можно решать на уровне статического анализатора.
И не забывали инициализировать unique_ptr
Эту проблему можно было бы решить, запретив нулевые указатели и убрав из unique_ptr конструктор по умолчанию, требуя тем самым, чтобы unique_ptr всегда содержал действительный указатель. Если нужно представить отстутствие указателя, то на помощь должна прийти система типов: optional<unique_ptr>. Здесь в Rust все сделали правильно.
И не делали случайное обращение к шареному ресурсу без синхронизации.
От проблем с шареными ресурсами и сихнронизацией вас ни один компилятор не спасет.vintage
09.01.2017 08:22Компилятор D спасает. Там по умолчанию у каждого потока своя память. А для межпотокового взаимодействия — либо сообщения, либо shared типы. Которые либо автоматически синхронизируются, либо вручную.
vintage
04.01.2017 13:48+1Если есть такое рвение приложить свою руку к созданию такого языка, то подключайтесь к разработке языка D. Там уже много классных плюшек. Например, недавно была статься про диапазоны: https://habrahabr.ru/post/318266/
ik62
04.01.2017 14:09dlang: хотите используете сборщик (по умолчанию), не хотите — не используете. Правда во втором случае теряется взаимодействие с теми частями стандартной библиотеки, которые на сборщик расчитывают. Компилятор имеет ключи для вывода информации о том, где вы могли нечаянно положиться на сборщик.
Синтаксис — С-подобный.
Ну и там еще много всего.
zxweed
> Например, на серверах, обслуживающих биржевые транзакции или поисковые запросы
эм, на биржевых серверах время отклика измеряется единицами и долями микросекунд, какие, к чёртовой бабушке, там 50 мс на сборщики мусора?
naething
Не все биржевые сервера — это high frequency trading. Если это сервер, именно обрабатывающий транзакции, а не скупающий акции ровно в момент их выставления на продажу, то зачем там микросекундный отклик?
zuborg
Задержка важна на любом сервере, который обрабатывает запросы, поступающие с высокой частотой.
Например, если у нас поступает 10К запросов с секунду, то за 200мс задержки размер очереди запросов (внезапно) увеличивается на 2К штук. Эти 2К запросов будут обработаны тоже с задержкой (независимой от GC), просто потому что ресурсов будет недостаточно, чтобы их все обрабатывать параллельно (условно, 1980 запросов будут ждать пока отработают 20, потом 1960 будут ждать завершения обработки следующих 20 шт и т.д.). При неблагоприятных условиях размер очереди будет дальше увеличиваться лавинообразно (говорю не понаслышке, встречал такие ситуации неоднократно — и из-за пауз GC, и из-за пауз при доступе к memcached и redis..)
Так что для серверного применения уменьшение длительности задержки в разы ценой 10-20% дополнительной нагрузки на CPU более чем оправдано.