LINUX.ORG.RU

Кто как борется с фактическим отсутствием приватных объявлений в C++?

 , ,


0

4

Как мы все знаем, private объявления де-факто являются частью интерфейса класса, их изменение приводит к перекомпиляции зависящего кода. Кто как обходит проблему? Из того, что я перепробовал:

 — непрозрачные ссылки на forward-объявления структур и функции для работы с ними;
 — публичная структура, которая агрегируется в класс-реализацию;
 — абстрактный класс, он же «интерфейс», от которого наследуется реализация.

Но у всех них есть свои недостатки. Есть ли какие-то иные приемы, которые я упустил?

★★★★

Последнее исправление: xaizek (всего исправлений: 1)

Ответ на: комментарий от byko3y

Без шуток, я бы так не смог.

Спасибо. А теперь, внимание, punch line (здесь должен быть drumroll): вот это вот всё - это примерно 15 MLOC heavily templated плюсового кода за’deploy’еных на тысячи серверов. Там в параллельной ветке кто-то пытался что-то говорить что C++ не жизнеспособен и вымирает? Ну да, ну да. И в контексте востребованности специалистов по плюсам: был бы человек хороший - с руками оторвем.

bugfixer ★★★★★
()
Последнее исправление: bugfixer (всего исправлений: 1)
Ответ на: комментарий от bugfixer

А теперь, внимание, punch line (здесь должен быть drumroll): вот это вот всё - это примерно 15 MLOC heavily templated плюсового кода за’deploy’еных на тысячи серверов

Извиняй, но я совсем не бум-бум в этой теме, так что не могу ни опровергнуть, ни подтвердить.

byko3y ★★★★
() автор топика
Ответ на: комментарий от byko3y

Си-подобные интерфейсы прекрасно налазят на любой язык, даже на питоны с хаскелями.

4.2, на питон оно налазит крайне плохо

интерфейсы попытались сохранить для старых пердунов, но реализацию всю переписали,

Все с точностью до наоборот

потому что ЦП-ориентированное HPC перестает быть HPC, если его выполнить на видеокарте.

Коллега при переводе своего HPC кода с ЦП на GPU просто добавил туда спецификаторов хост/девайс, код от этого не перестал быть HPC.

В общем Вы как обычно несете лютую чушь по темам в которых ничего не понимаете.

AntonI ★★★★★
()
Последнее исправление: AntonI (всего исправлений: 1)
Ответ на: комментарий от AntonI

Я немного разовью тему для мимо-читателей. На определенном уровне абстракции CPU вообще можно отождествлять с GPU, проектируя параллельные алгоритмы.

Например, пользуясь схемой «одно ядро CPU = один мультипроцессор GPU» и «DDR = GDDR». Возможны и другие схемы отождествления.

Соответственно, и параллельный код можно писать единообразно на некотором уровне. Те самые шаблоны и другая кодогенерация, от которой тут многие так плюются.

Естественно, где-то «внизу» уровней абстракции будет «железный» код, и под CPU и GPU он будет разный, конечно.

Коллега при переводе своего HPC кода с ЦП на GPU просто добавил туда спецификаторов хост/девайс, код от этого не перестал быть HPC

В общем случае этого, конечно, недостаточно. Но бывают приятные исключения.

Тема сходства и различий параллельных алгоритмов, равно и переиспользования кода под CPU и GPU намного глубже, чем может показаться.

Crocodoom ★★★★★
()
Ответ на: комментарий от byko3y

Продолжу мысль. Если у нас есть бригада в 10 и более человек, у которых понятные роли, если мы создаём, что-то типа библиотек (stl, Qt, OpenCV и т.д.), то C++ - идеальный инструмент. Ну и пусть программа компилируется час, пусть день. У нас в данном случае и настоящих дедлайнов нет - финансирование идёт независимо от выполнения в срок.

Если команда 1-3 человека и есть дедлайны, при срывах которых мы будем работать в убыток, то C++ может создать проблемы. Здесь раньше выручал питон и его аналоги. Возможно, и сейчас выручает. Если перечислить в порядке убывания важности его особенности, обеспечивающие ускорение разработки, то будет как-то так:

  1. Отсутствие долгой компиляции. Это не требует комментариев.
  2. Простой синтаксис, не требующий гугления на каждый шаг. В крайнем случае, есть оффлайн документация, охватывающая не только синтаксис, но и богатую стандартную библиотеку.
  3. Сама стандартная библиотека. Тут и удобные контейнеры (строки, списки, словари), и работа с файловой системой, xml, json, сеть, и т.д.
  4. Сборщик мусора.
  5. Динамическая типизация.

Сейчас питон частично уходит от пунктов 2, 5, но это, вероятно, приемлемо для программистов.

Но если мы берём C, Java, C#, Delphi, то и они будут превосходить C++ по части из моего списка. Тут C выигрывает по второму пункту, а остальные языки по третьему. Понятно, что по первому они все превосходят C++. При этом, все эти языки позволяют создавать приложения, которые работают быстрее приложений на питоне. Если это важно, то надо искать язык, у которого выполняется пункты 1, 2, 3. По ощущениям - это C#, но только с мощной поддержкой Visual Studio.

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

Kogrom
()
Ответ на: комментарий от Kogrom

По ощущениям - это C#, но только с мощной поддержкой Visual Studio

Да. MS повторило жаву, только правильно, с подключением нативных либ, позже добавило в язык человечных фич, из-за чего Java начала терять позиции угрожающими темпами, даже несмотря на то, что портируемость у шарпа намного хуже. Шарп давно бы укатал жаву в асфальт, а также кучу смежных технологий, вроде тех же крестов в GUI, если бы мелкомягкие изначально не цеплялись так за монополию своей платформы. В вендоролоке есть такой прикол, что ты либо царь, либо пропал, поскольку все пользователи могут оказаться по другую сторону созданного тобой же барьера, где они являются не твоими пользователями — и в данном случае MS где-то в районе 2010-2012 пропал по полной.

А сейчас уже появились иные альтернативы, как то Electron, на котором нынче сам же MS и пишет десктоп. «Пропал» MS в данном случае в качестве монополии — так-то вакансий по C# в полтора раза больше, чем по C и C++ вместе взятым.

Взять Паскаль, сделать его синтаксис более лаконичным и получим требуемое

Да, да, да. Borland похоронил паскаль, во-первых поставив абсолютно неадекватные цены без каких-либо бесплатных версий даже для простой сборки сорцов, а во-вторых потому что не развивал его, а тупо копировал рандомные фичи с C++. В 80-е же люди испытывали культурный шок, когда приложение на пару тысяч строк на очень слабых (по нынешним меркам) компах заканчивало компилироваться быстрее, чем ты отпускал F9. Я верил в паскаль, но я тогда не понимал рыночка, а рыночек нынче движется западом, и он таков, что его никогда не колышет техническая составляющая, потому что среди принимающих решения технически грамотных людей почти нет. И на это очень важно ориентироваться, когда ты пытаешься в рыночек влиться, поскольку быстрый результат и вау-эффект значат всё, а тщательная техническая продуманность никогда не играла роли.

И я сейчас не ною «гады, проклинаю вас, не оценили такого смышленного спеца», а жалею про упущенные годы, когда Я ЭТОГО НЕ ЗНАЛ и никто мне этого не пояснил — и люди продолжают не осознавать положения вещей, по крайней мере пока гром не грянет.

byko3y ★★★★
() автор топика
Ответ на: комментарий от byko3y

рыночек

Тут не рыночек уже, а корпоративные тонкости. Например, приходит новое начальство и говорит, что его главная цель - в конце года покачать акционерам красивый график с экономическими показателями. Как это сделать? Прибыль увеличить сложно, зато легко можно выиграть за счёт уменьшения издержек. Какие у нас издержки? Зарплата сотрудников. Создаём условия, при которых половина сотрудников уйдёт - и достигаем кратковременную цель.

После демонстрации красивого графика уже можно нанять новых сотрудников, чтобы всё окончательно не загнулось. Новые программисты могут быть фанатами языков программирования, отличных от тех, что использовались ранее. В результате, после нескольких итераций обновления коллектива мы получаем зоопарк технологий.

И вот у нас и Java, и C#, и C++, и даже немного Delphi c Python. Хорошо ещё, если какого-нибудь Пролога с ОКамлом нет. Кого брать на поддержку и развитие этого безобразия? Конечно, плюсовика. Потому что если он с плюсами разобрался, то и со всем остальным как-то справится.

Kogrom
()
Ответ на: комментарий от Kogrom

Например, приходит новое начальство и говорит, что его главная цель - в конце года покачать акционерам красивый график с экономическими показателями

Давай начнем с того, что такая задача в силиконовой долине стоит почти всегда, И даже серьезными корпорациями там управляют финансисты. И прежде всего они понимают, что новый стэк технологий — это лишние затраты. Как правило, они просто берут технологии с первой страницы гугла — просто потому, что «проверено крупнейшими игроками рынка, индустриальный уровень, web-scale» — это базворды, приятные уху венчурного инвестора.

И вот у нас и Java, и C#, и C++, и даже немного Delphi c Python. Хорошо ещё, если какого-нибудь Пролога с ОКамлом нет. Кого брать на поддержку и развитие этого безобразия? Конечно, плюсовика

«Ты Плюсовик?... Лампочку вкрути». По моим впечатлениям, плюсовики не особо дружат с другими технологиями, поскольку такое знакомство очень часто формирует стойкое отвращение к C++: когда ты видишь, что какой бы ты аспект крестов не взял — крестовая реализация будет намного хуже аналогичной в других языках. Реализовано «всё и ничего» с выраженным акцентом в сторону «ничего». И тогда ты перестаешь быть «плюсовиком».

byko3y ★★★★
() автор топика
Ответ на: комментарий от Crocodoom

В общем случае этого, конечно, недостаточно. Но бывают приятные исключения.
Тема сходства и различий параллельных алгоритмов, равно и переиспользования кода под CPU и GPU намного глубже, чем может показаться

Ну что же ты так неуверенно, давай просто возьмем и прогоним его ссаными тряпками, а то чел попутал рамсы и кодеру-железячнику рассказывает про особенности работы процессора. Мне для публики из одного человека стараться неинтересно, рассказывая, почему поддержка ЦП-подобного кода была чисто маркетинговым ходом, чем все-таки отличаются эти процессоры, и даже лень было написать, что Nvidia всю реализацию LAPACK переписала. Я тему GPGPU поднимал здесь уже раз эдак пять за год, что-то LOR вяло реагирует на глубокие технические темы.

byko3y ★★★★
() автор топика
Ответ на: комментарий от byko3y

Ну что же ты так неуверенно, давай просто возьмем и прогоним его ссаными тряпками

Не надо никого никуда прогонять. Ценность дискуссии мультипликативно зависит от числа активных участников, так что.

а то чел попутал рамсы и кодеру-железячнику рассказывает про особенности работы процессора.

Да понты из тебя так и хлещат. А остальные тут, видимо, только hello-world в онлайн-эмуляторе писали.

Мне для публики из одного человека стараться неинтересно, рассказывая, почему поддержка ЦП-подобного кода была чисто маркетинговым ходом

Маркетинг обсуждайте без меня

чем все-таки отличаются эти процессоры

А вот это уже интересно. Чем отличаются CPU и GPU по мнению byko3y?

даже лень было написать, что Nvidia всю реализацию LAPACK переписала.

Естественно, инженеры Nvidia заточили код под свою железяку. А если бы LAPACK был на питоне, то он типа лёг бы на <архтектурно новую> GPU как родной? Это такой троллинг тупостью?

Crocodoom ★★★★★
()
Ответ на: комментарий от Crocodoom

Ценность дискуссии мультипликативно зависит от числа активных участников, так что

Вы, когда принимаете решение по архитектуре, наверное зовете всех бомжей с улицы?

Да понты из тебя так и хлещат. А остальные тут, видимо, только hello-world в онлайн-эмуляторе писали

Я разве пишу про остальных?

Чем отличаются CPU и GPU по мнению byko3y?

GPU по сути представляют собой примитивные процы образца 90-х годов, но на новой элементной базе: мало кэша, короткий конвеер с in-order execution, медленная оперативная память. Самое главное, это значит абсолютно никчемную скорость однопоточного выполнения, которое разгоняли на десктопных процессорах последние 30 лет. Это значит, что при достаточно кривых руках и однопотоковом выполнении ядра твоя топовая видеокарта будет бегать, как десктопный комп эдак 2005 года выпуска. К такой ситуации приводят операции, которые могут выполняться строго поочередно, как то «безобидный» стэк в оперативной памяти, косвенная адресация, виртуальные функции и просто указатели на функции. Напоминаю, что в ранних GPGPU никаких вызовов функций на уровне машинного кода не было, все функции инлайнились.

Да, ЦПУ-шный SIMD портировать на GPGPU проще, но если речь идет про x86, то там все-таки подходы сильно отличаются, ведь у ЦП опять-таки акцент последовательный расчет коротких массивов операндов, а у GPGPU — на сотни потоков.

Многопоточный код с ЦП вообще не налазит на GPGPU, поскольку у видюх некогерентный кэш и никаких там мутексов на compare-and-swap нет, и вообще в маш кодах нет compare-and-swap.

Я почему и завел разговор про маркетинг: Nvidia очень постаралась сделать впечатление, что этих проблем нет, что ты просто компилируешь обычный сишный код nvcc, и получаешь рост производительности. Но одно неосторожное условие — оптимизация ломается, и ты получаешь чудовищный провал по производительности с неочевидными причинами. А если ты при этом пишешь на C++, то причины могут быть еще менее очевидными. То есть, подход на самом деле контрпродуктивный, но пипл хавает.

Естественно, инженеры Nvidia заточили код под свою железяку. А если бы LAPACK был на питоне, то он типа лёг бы на <архтектурно новую> GPU как родной? Это такой троллинг тупостью?

Ничего тебе не «очевидно». Тебе не очевидно, что нвидии пришлось выкинуть некоторые функции из лапака, поскольку их просто нельзя было эффективно реализовать на GPGPU.

byko3y ★★★★
() автор топика
Ответ на: комментарий от byko3y

GPU по сути представляют собой примитивные процы образца 90-х годов, но на новой элементной базе: мало кэша

На каких задачах тебе не хватило кэша GPU? Мне пока хватает

короткий конвеер с in-order execution

Ок

медленная оперативная память

GDDR6 медленнная? По ср. с чем?

Это значит, что при достаточно кривых руках и однопотоковом выполнении ядра твоя топовая видеокарта будет бегать, как десктопный комп эдак 2005 года выпуска. К такой ситуации приводят операции, которые могут выполняться строго поочередно, как то «безобидный» стэк в оперативной памяти, косвенная адресация, виртуальные функции и просто указатели на функции

Сдуру можно и х*й сломать, это правда

Да, ЦПУ-шный SIMD портировать на GPGPU проще, но если речь идет про x86, то там все-таки подходы сильно отличаются, ведь у ЦП опять-таки акцент последовательный расчет коротких массивов операндов, а у GPGPU — на сотни потоков

Здесь +

Многопоточный код с ЦП вообще не налазит на GPGPU, поскольку у видюх некогерентный кэш и никаких там мутексов на compare-and-swap нет, и вообще в маш кодах нет compare-and-swap.

«Многопоточные коды с ЦП» разные бывают

// Про маркетинг no comments

Ничего тебе не «очевидно». Тебе не очевидно, что нвидии пришлось выкинуть некоторые функции из лапака, поскольку их просто нельзя было эффективно реализовать на GPGPU.

Продолжайте ваши наблюдения

Crocodoom ★★★★★
()
Ответ на: комментарий от Crocodoom

На каких задачах тебе не хватило кэша GPU? Мне пока хватает

Проблема не в том, что его «не фатает», а в том, что его по сути нет, большая часть «кэша» тусит в регистрах процессоров, а не в каком-то отдельном быстродействующем хранилище. У тебя на 3080 есть 8700 ядер и 8700 кб L1 кэша:
https://images.nvidia.com/aem-dam/en-zz/Solutions/geforce/ampere/pdf/NVIDIA-a...
То есть, по килобайту на ядро. При том, что регистров по 2 кб на ядро (в два раза больше) — типовая картина для процессора с некогерентным кэшем, которому не нужно координироваться на кэше более высокого уровня и который часто грузит данные из оперативной памяти сразу в регистры, мимо кэшей.

Сравни это с типичным современным десктопным процессором, у которого 1.5 МБ когерентного кэша на ядро.

GDDR6 медленнная? По ср. с чем?

По сравнению с L2/L3 кэшем. Любая оперативная память медленная. Здесь под скоростью я имею в виду именно задержку доступа, а не ширину канала — так-то ширина у GDDR6 составляет сотни гигабайт в секунду. Речь идет, грубо говоря, про сотню циклов на доступ, и чем больше подходов чтения нужно поочередно выполнить, тем меньше будет производительность твоего кода. Отсюда неэффективность косвенного доступа и виртуальных методов.

«Многопоточные коды с ЦП» разные бывают

Основной примитив синхронизации на ЦП — это атомарный compare-and-swap, на котором реализовываются как мутексы, так и lock-free алгоритмы. Всё это не работает на видеокарте, потому что на ней нет когерентного кэша, который бы являлся арбитром при выполнении compare-and-swap. Вместо этого основной механизм синхронизации на GPGPU — барьеры плюс ручное согласование кэша.

byko3y ★★★★
() автор топика
Последнее исправление: byko3y (всего исправлений: 1)
Ответ на: комментарий от byko3y

Я 30-300 строк кода написал и ушел другие дела делать. А датасет сутки может обрабатываться. Всё от объёмов и сложности того что делаешь зависит.

peregrine ★★★★★
()
Ответ на: комментарий от byko3y

стойкое отвращение к C++

Если что-то можно сделать не на C++, то это и надо делать не на C++. Но есть случаи, когда C++ уместен. Когда нужна высокая скорость исполнения или когда освобождение ресурсов нужно в строго определённый момент. Да, в языках с GC есть принудительный GC.Collect(), но тут ничего не гарантируется. Кроме того, если нам надо освободить не память, а какое-то устройство, то используется Dispose(), а не GC. И в Dispose() тоже может быть некорректный код, который не даст освободить устройство. И это хуже утечек памяти.

Есть ещё такой момент - обработка ошибки может быть корректной, но запоздалой или просто бессмысленной. Я часто встречаю код на си, в котором перехватываются все возможные ошибки, но дальше автор не знает, что с этим перехватом делать. Пытается выводить в консоль «В используемой библиотеке что-то не так». Будет ли пользователю от этого лучше?

Kogrom
()
Ответ на: комментарий от byko3y

У тебя на 3080 есть 8700 ядер и 8700 кб L1 кэша. То есть, по килобайту на ядро
...
Сравни это с типичным современным десктопным процессором, у которого 1.5 МБ когерентного кэша на ядро.

Схема архитектур следующая. Несколько упрощённо, как я её себе представляю, буду рад замечаниям.

Nvidia RTX 3080 [1]

P0: 16 float / cycle = half-warp
P1: 2 datapaths = warp
P2: 4 processing blocks = Streaming Multiprocessor (SM)
// P0-2: 128 Cuda Cores
P3: 68 SM = GPU

L1: 128 Кб на 1 SM
L2: 6 Мб общий для всех SM

Intel i9-9990XE [2]

P0: 16 float / cycle = SIMD
P1: 2 threads = HyperThreading
P2: 14 cores = CPU

L1: 64 Kb на 1 core
L2: 1 Mb на 1 core
L3: 1.375 Mb на 1 core // удивился, что даже к L3 у каждого ядра свой канал (?)

Теперь переходим собственно к сравнению. L1 на GPU и CPU примерно равны по размерам, я также предлагаю считать SM и core эквивалентыми сущностями. Тогда получается, что на CPU 14 <ядер>, а на GPU 68 <ядер>, и у каждого <ядра> есть небольшой, но очень быстрый, когерентный L1. Суммарно L1 у GPU существенно больше CPU, так надо обслуживать большое количество FPU. Но принципиальных отличий не вижу.

Что касается других кэшей, то Intel L2+L3 (сюда же Intel Smart Cache) действительно выглядит превосходящим GPU L2. Пригодился ли бы более развитый L2 на GPU? Конечно. Но пока работаем с тем, что есть.

Далее, те «ядра», коих byko3y насчитал 8700 — это Cuda Cores, их как отдельных сущностей на чипе не существует. Лучше говорить о 8704 FP / cycle, перемножив все уровни параллельности P#. Для CPU получается 448 FP / cycle. А чему там соответствует кэш какого уровня — надо аккуратно следить. Подробнее про уровни параллельности писал в [4].

По сравнению с L2/L3 кэшем. Любая оперативная память медленная. Здесь под
скоростью я имею в виду именно задержку доступа, а не ширину канала — так-то
ширина у GDDR6 составляет сотни гигабайт в секунду. Речь идет, грубо говоря,
про сотню циклов на доступ, и чем больше подходов чтения нужно поочередно
выполнить, тем меньше будет производительность твоего кода.

Волга впадает в Каспийское море. Лошади кушают овес и сено (с)

В простых случаях кэширование «просто работает». Да, производительность кода м.б. не идеальной... но вполне достаточной. Сильно зависит от задачи. Но конечно, если очень хочется, то можно и нужно писать cache-aware алгоритмы [next lvl: cache-oblivious]

неэффективность косвенного доступа и виртуальных методов.

Здесь +

Основной примитив синхронизации на ЦП — это атомарный compare-and-swap, на
котором реализовываются как мутексы, так и lock-free алгоритмы. Всё это не
работает на видеокарте, потому что на ней нет когерентного кэша, который бы
являлся арбитром при выполнении compare-and-swap. Вместо этого основной
механизм синхронизации на GPGPU — барьеры плюс ручное согласование кэша.

В CUDA тоже есть atomicCAS(), бумс. Но я делал синхронизацию даже без атомиков, в порядке эксперимента [5]. Там volatile главное проставить на мьютексе и на данных.

Также есть официальное API для кооперативной работы SM-ов (и не только их) [3]


[1] https://images.nvidia.com/aem-dam/en-zz/Solutions/geforce/ampere/pdf/NVIDIA-a...

[2] https://en.wikichip.org/wiki/intel/core_i9/i9-9990xe

[3] https://developer.nvidia.com/blog/cooperative-groups/

[4] https://link.springer.com/chapter/10.1007/978-3-030-92864-3_29

[5] https://link.springer.com/chapter/10.1007/978-3-030-36592-9_5

Crocodoom ★★★★★
()
Последнее исправление: Crocodoom (всего исправлений: 1)
Ответ на: комментарий от Crocodoom

Intel i9-9990XE [2]
P1: 2 threads = HyperThreading

Там не в 2 раза больше вычислителей, а просто продублирован набор регистров. На практике это значит примерно 25% прироста производительности. CUDA тоже умеет вешать до 1024 потоков на один SM — получатся еще более веселые цифры, так что я советую отложить гипертреды в сторону. В то же время, SM на оптимальной программе может действительно выполнять 128 операций за цикл. Другое дело, что не всегда получается достичь этой производительности — это да.

128 операций за цикл против 16 SP за цикл. Восьмикратная разница, однако.

Теперь переходим собственно к сравнению. L1 на GPU и CPU примерно равны по размерам, я также предлагаю считать SM и core эквивалентыми сущностями. Тогда получается, что на CPU 14 <ядер>, а на GPU 68 <ядер>, и у каждого <ядра> есть небольшой, но очень быстрый, когерентный L1. Суммарно L1 у GPU существенно больше CPU

Когерентность — это свойство системы, а не одиночной сущности. Потому модель называется просто «у каждого ядра есть свой L1 кэш». Этот L1 кэш обычно не когерентен ни с L2, ни с оперативной памятью, ни с другими L1 кэшами.

Я не вижу смысла рассуждать об L1 обособленно. У GPGPU L2 еще меньше, чем L1. При этом, у ЦП L1 кэш таки когерентен с другими, а у GPGPU — нет. Но даже если считать по L1 на операцию за цикл, то у ЦП все равно L1 в 4 раза больше. Но, еще раз, в отрыве от остальных кэшей это бессмысленная цифра.

Что касается других кэшей, то Intel L2+L3 (сюда же Intel Smart Cache) действительно выглядит превосходящим GPU L2. Пригодился ли бы более развитый L2 на GPU? Конечно. Но пока работаем с тем, что есть

Увеличение кэша не является бесплатной штукой, иначе бы у нас давно на процессорах везде были бы кэши по 64 МБ.

По сравнению с L2/L3 кэшем. Любая оперативная память медленная

Волга впадает в Каспийское море. Лошади кушают овес и сено

Неожиданный факт: ширина канала к L3 кэшу у i9-9900K эдак в 2 раза меньше, чем ширина канала к оперативе у RTX 3080. То есть, оператива де-факто выполняет функцию L3 кэша. Опять-таки, хотя названия похожи, но механизмы работы узлов на самом деле разительно отличаются, потому прямо приравнивать одно к другому некорректно. Например, у нвидии L1 кэш и разделяемая память находятся в одной области, за счет чего можно забирать память у L1 кэша и отдавать ее под разделяемую память. Плюс само кэширование довольно гибко настраивается, потому что часто автоматический алгоритм мешает производительности, а не помогает.

В CUDA тоже есть atomicCAS(), бумс

Мда, Акелла промахнулся, таки действительно есть. Но в любом раскладе, если в тупую портировать ЦП-шные мутексы на GPGPU, то получится вот это:

https://stackoverflow.com/questions/21341495/cuda-mutex-and-atomiccas

byko3y ★★★★
() автор топика
Ответ на: комментарий от Kogrom

Но есть случаи, когда C++ уместен. Когда нужна высокая скорость исполнения или когда освобождение ресурсов нужно в строго определённый момент. Да, в языках с GC есть принудительный GC.Collect(), но тут ничего не гарантируется

Однако, достаточно сложные системы на C++ неизбежно приходят к GC. Дело не в том, что «нужно или нет», а в том, что не нужно использовать GC там, где оно не нужно, и нужно использовать там, где нужно. Высвобождение сложносвязанных объектов на счетчиках будет не быстрее иной синхронной сборки мусора.

Я часто встречаю код на си, в котором перехватываются все возможные ошибки, но дальше автор не знает, что с этим перехватом делать. Пытается выводить в консоль «В используемой библиотеке что-то не так». Будет ли пользователю от этого лучше?

Ты описал Си-специфичную проблему. Даже в паскалях с коболами такого трешака нету. На Си ты должен всё описывать руками, прямо всё-привсё, как на ассемблере. Конечно, можно сделать макросы для сокращения объема кода, но получится еще хуже в плане читаемости.

byko3y ★★★★
() автор топика
Ответ на: комментарий от byko3y

Высвобождение сложносвязанных объектов на счетчиках будет не быстрее иной синхронной сборки мусора.

в плюсах на счетчиках пишут только лохи. сборку мусора в плюсах не сделать вовсе в силу тайп_кастов, юнионов, и адресной арифметики, когда реальный указатель может быть скрыть за невинным типом или вычисляется на лету.

а сама сборка мусора это высокие накладные расходы как по памяти, так и времени выполнения. подходит только для скриптухи и шлепоформ, где производительность не выжимают и на размер потребляемой памяти не смотрят.

alysnix ★★★
()
Ответ на: комментарий от alysnix

а сама сборка мусора это высокие накладные расходы как по памяти, так и времени выполнения. подходит только для скриптухи и шлепоформ, где производительность не выжимают и на размер потребляемой памяти не смотрят

Если у тебя сложный асинхронный код с кучей объектов, которые непонятно кем используются и непонятно когда их высвобождать, то у тебя остается не сильно много выбора. А это картина, актуальная для почти любой сложной системы. И в том числе для ядра линя, в котором есть сборка мусора по загружаемым модулям.

byko3y ★★★★
() автор топика
Ответ на: комментарий от byko3y

которые непонятно кем используются и непонятно когда их высвобождать

все примеры это напустить воды что всё ужасно сложно и непонятно, непонятно что делает программа, непонятно как работает, всё течет и падает, главное асинхронно, а виноват С++.

ОК :(

fsb4000 ★★★★★
()
Ответ на: комментарий от byko3y

ref counting - это не сборка мусора, а обычный механизм контроля использования ресурса.

при использовании явно пишут типа: use(this_object)

при конце использования пишут: unuse(this_object)

как только счетчик использований(инкрементируемый/декрементируемый в use/unuse ) == 0, обьекту делается delete внутри unuse(…)

это не сборка мусора вообще, это ручное управление освобождением обьекта, методом подчета счетчика использования.

alysnix ★★★
()
Ответ на: комментарий от byko3y

Однако, достаточно сложные системы на C++ неизбежно приходят к GC.

Это будет локальный GC. Например, если мы используем xml, json или даже базу данных, то управление памятью для этих конкретных данных можно поручить конкретной библиотеке. Остальной код остаётся без GC.

Но ты игнорируешь одну вещь. В сложных системах связь между объектами не обязательно только владение. И тут GC никак не поможет.

Приведу пример. Допустим, мы делаем стратежку, где один юнит может защищать другого. При этом телохранитель получает 50 % урона, наносимого защищаемому. Когда один из этих юнитов уничтожается, то второй должен изменить свой статус - он больше не телохранитель или защищаемый, но по прежнему существует. Попробую даже сообразить хранение данных этих юнитов от простого к сложному.

  1. Берём для отряда vector, а данные юнитов будут в структурах. Связь защищаемого и телохранителя тупо по индексам vector-а. Естественно, так работать не будет, потому что удаление какого-то третьего юнита приведёт к тому, что все индексы поедут.
  2. Вместо индексов будем использовать указатели. Это тоже не поможет, так как при удалении элемента в vector-е все указатели поменяются.
  3. Не будем хранить структуры в vector-е прямо, а заменим их на умные указатели. Или оставим структуры, но вместо vector-а возьмём list. Тут вроде бы указатели не уедут. Но как сделать сохранение и загрузку?
  4. Возвращаемся к vector-у. Вместо указателей используем свои уникальные идентификаторы (типа int, например). Оповещением об изменении статуса теперь занимается отдельный объект (функция), а не сам юнит.

Мораль тут такая: в случае со сложносвязанными объектами нам не только GC не понадобился, но и умные указатели. Конечно, можно придумать абстрактный пример, где GC непременно нужен. На практике же он нужен обычно там, где плохая архитектура. Хотя соглашусь, что в состоянии вечного стресса и дедлайнов нормальной архитектуры не будет.

Ты описал Си-специфичную проблему.

Я описал проблему языков без исключений. Если нет исключений, но есть паники, то это тоже вариант.

Kogrom
()
Ответ на: комментарий от Kogrom

Не будем хранить структуры в vector-е прямо, а заменим их на умные указатели. Или оставим структуры, но вместо vector-а возьмём list. Тут вроде бы указатели не уедут. Но как сделать сохранение и загрузку?
Возвращаемся к vector-у. Вместо указателей используем свои уникальные идентификаторы (типа int, например). Оповещением об изменении статуса теперь занимается отдельный объект (функция), а не сам юнит

Пример плох по нескольким причинам. Прежде всего, стоимость доступа O(log N) в горячем коде часто становится неприемлимой. Только O(1). Во-вторых, у нас что, кроме этой логики больше ничего нет? По сути ты описал только один самодостаточный контейнер и процедуру удаления сущностей из него. А как ты решишь, что эта самая сущность, юнит, должна высвобождаться? А если она высвободилась — как гарантировать, что «уникальный» идентификатор действительно будет уникальным и под раздачу не попадет случайный новый объект? (знаменитая проблема ABA)

Кстати, у Blizzard была похожая проблема:
https://www.codeofhonor.com/blog/tough-times-on-the-road-to-starcraft
Да, скорее всего они в плюсовом коде полностью руками управляют памятью и время жизни у них определяется логикой, то есть, «вот в этот момент у нас ссылок на объект больше нигде не может быть, мы можем просто высвободить память» — учитывая тот факт, как часто падает SC 2 при обратной перемотке игры, когда нормальное течение логики нарушается. Правда, в скриптах у них диалект Lua со статической типизацией, который в принципе не работает без сборки мусора.

Я описал проблему языков без исключений. Если нет исключений, но есть паники, то это тоже вариант

Кстати, да, падать по необработанной ошибке — тоже годный вариант. Сейчас возникли адвокаты паник Rust, но каких-нибудь лет 30 назад ВСЕ языки были такими как Rust... кроме ассемблера. Да, порой логика глючила, попрыгав по десятку goto, но ошибочный код не приводил к исполнению данных на стэке. Я писал, что проблема Си-специфична потому, что ни один другой язык (кроме асма) не выдает кодеру такое количество арсенала для стрельбы себе в ногу. Даже для низкоуровневого языка не обязательно было делать всё так плохо. Даже паскаль с рекурсиями и динамическим выделением памяти не позволял выполнять стэк.

byko3y ★★★★
() автор топика
Ответ на: комментарий от byko3y

Пример плох по нескольким причинам.

Пример предназначен только для того, чтобы показать, что очень часто бывают связи между объектами, когда GC никак не поможет. В конкретику я ушёл, чтобы ты задержал взгляд.

А когда связи только отвечают за владение, то и без GC обычно можно справиться.

Прежде всего, стоимость доступа O(log N) в горячем коде часто становится неприемлимой.

Это всё мелочи, пока число юнитов не достигло десятков тысяч. Да, можно в простой map засунуть без всяких хешей. А можно даже обходить vector по порядку - это всё равно будет не самое медленное в коде.

А как ты решишь, что эта самая сущность, юнит, должна высвобождаться?

Кончились очки жизни - удаляем юнит. Функция нанесения ущерба юниту проверит и запустит удаление, а также оповещение других юнитов.

как гарантировать, что «уникальный» идентификатор действительно будет уникальным и под раздачу не попадет случайный новый объект?

Если говорить конкретно про оффлайн стратежку, то проблем нет. Объектов не будет бесконечное количество. Можно без всяких хешей жить спокойно. Просто инкрементировать счётчик на каждый новый созданный объект. С онлайн-стратежкой идентификатор надо будет запрашивать у сервера.

Kogrom
()
Последнее исправление: Kogrom (всего исправлений: 1)
Ответ на: комментарий от Kogrom

Пример предназначен только для того, чтобы показать, что очень часто бывают связи между объектами, когда GC никак не поможет. В конкретику я ушёл, чтобы ты задержал взгляд.
А когда связи только отвечают за владение, то и без GC обычно можно справиться

А, ты об этом. Ну как бы я за GC топил не для того, чтобы стройные деревья владений чистить — в асинхронных системах очень часто вообще непонятно, кто кем владеет. Опять же, если взять в твоем примере несколько параллельных алгоритмов, которые потенциально будут развязывать-связывать юнитов, то внезапно выяснится, что нет никаких более способов понять, кого нужно высвобождать, а кого — еще нет, кроме как сборка мусора.

Это всё мелочи, пока число юнитов не достигло десятков тысяч. Да, можно в простой map засунуть без всяких хешей. А можно даже обходить vector по порядку - это всё равно будет не самое медленное в коде

Если только два юнита взаимодействуют, то да. Если же взаимодействий больше числа юнитов, то становится невесело. Тот же движок SC2/WC3 для скриптов дает интерфейс ассоциативного массива, которого фатает для простых штук. Например, DoTA реализует на скриптах «диспетчеризацию» каждой единицы урона, которых может возникать сотни в секунду, по массивам и ассоциативным массивам — и в большинстве случае справляется. А вот скрипты в SC2 co-op с мутаторами уже не вывозятся.

Кончились очки жизни - удаляем юнит. Функция нанесения ущерба юниту проверит и запустит удаление, а также оповещение других юнитов

Ты предлагаешь вообще любой доступ к юниту делать через его ключ, а не по указателям? Ну типа да, это надежно, но это же десятки-сотни O(log N) доступов на каждый тик.

Если говорить конкретно про оффлайн стратежку, то проблем нет. Объектов не будет бесконечное количество. Можно без всяких хешей жить спокойно. Просто инкрементировать счётчик на каждый новый созданный объект. С онлайн-стратежкой идентификатор надо будет запрашивать у сервера

В принципе, int128_t задачу решает даже без запросов на сервер. Только жирноват будет для горячего кода.

byko3y ★★★★
() автор топика
Ответ на: комментарий от byko3y

ты гонишь. сборщик всего лишь освобождает по сочетанию формальных правил - нужна свободная память && счетчик ссылок == 0 && обьект недостижим из корня иерархии обьектов.

никакого реального отношения к «правильной последовательности» освобождения это не имеет. если обьекты A и B удовлетворяют данному правилу, то порядок их уничтожения неспецифицирован.

более того, именно ручное уничтожение задает порядок, а автоматическое - нет.

alysnix ★★★
()
Ответ на: комментарий от byko3y

SC2

Да, я думал о чём-то более простом, возможно, даже пошаговом. Для старкрафта лучше комбинировать - идентификаторы только для загрузки и сохранения, а указатели во время игры. Тогда O(log N) будет только вначале и в конце.

Многопоточность тут ничего не меняет. Нам нужно изменить состояние одного юнита при удалении другого. GC такое не делает. Он не оповещает, а удаляет. А если бы даже оповещал, то делал бы это непонятно когда. У нас бы один юнит уже помер, а второй ещё некоторое время его охранял.

Kogrom
()
Ответ на: комментарий от alysnix

никакого реального отношения к «правильной последовательности» освобождения это не имеет. если обьекты A и B удовлетворяют данному правилу, то порядок их уничтожения неспецифицирован

Вот только что писал в соседнем треде про shared_ptr и то, что финализация и высвобождение памяти — это совершенно разные процессы:

юнионы в C++ (комментарий)

более того, именно ручное уничтожение задает порядок, а автоматическое - нет

Вот, и потому shared_ptr мешает правильно задать порядок финализации.

byko3y ★★★★
() автор топика
Ответ на: комментарий от Kogrom

Многопоточность тут ничего не меняет. Нам нужно изменить состояние одного юнита при удалении другого. GC такое не делает. Он не оповещает, а удаляет. А если бы даже оповещал, то делал бы это непонятно когда. У нас бы один юнит уже помер, а второй ещё некоторое время его охранял

Давай все-таки сконцентрируемся на высвобождении объектов. Я изначально писал о том, что GC позволяет безопасно высовбождать объекты в сложных системах, и альтернатив этому нет. Я не спорю, что логика обработки умирания юнита должна быть написана руками, хотя и там может быть варианты помимо простого «синхронно вызвать колбэк». А вот когда вся логика отработала (кстати, когда она закончила работать?) — нам нужно высвободить объект, желательно сделав это так, чтобы потом на этот объект не остались висеть указатели. Это можно неэффективано сделать на списках слабых ссылок (двунаправленная ссылка), можно тоже неэффективно сделать через постоянный запрос в ассоциативном массиве, а можно сделать через GC, который для относительно малого числа объектов-связей (даже для десятков тысяч юнитов) будет выполняться очень быстро, быстрее расчета одного шага времени.

byko3y ★★★★
() автор топика
Ответ на: комментарий от byko3y

Вот, и потому shared_ptr мешает правильно задать порядок финализации.

что shared, что GC - это автоматическое высвобождение.

все нормальные GC к тому ж ссылки на обьект считают, то есть работают как shared_ptr в c++, если могут,потому что так быстрее найти мусорный обьект. для остальных обьектов применяют обход или типа того.

так что у GC те же пороки, что и у shared_ptr.

alysnix ★★★
()
Ответ на: комментарий от alysnix

все нормальные GC к тому ж ссылки на обьект считают
так что у GC те же пороки, что и у shared_ptr

Так я ж не спорю. Я писал про то, что GC — это shared_ptr здорового человека по высвобождению памяти. Проблему порядка финализации он не решает — я об этом и не писал.

byko3y ★★★★
() автор топика
Ответ на: комментарий от byko3y

Давай все-таки сконцентрируемся на высвобождении объектов. Я изначально писал о том, что GC позволяет безопасно высвобождать объекты в сложных системах, и альтернатив этому нет.

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

Вот есть спагетти-код, который осуждают большинство программистов. А есть ещё спагетти-данные, которые непонятно как генерятся и связываются. Есть шаблоны GoF, дающие способы создания спагетти данных. Но может если ограничить себя, как это сделали с GOTO и не делать спагетти-данных, то код станет проще и предсказуемее? Я пока это не утверждаю. Только предполагаю.

Kogrom
()
Ответ на: комментарий от byko3y

Ты предлагаешь вообще любой доступ к юниту делать через его ключ, а не по указателям? Ну типа да, это надежно, но это же десятки-сотни O(log N) доступов на каждый тик.

Можно «удалять» юнит через помещение его адреса в список свободных. И когда нужен новый юнит, то сначала брать адрес из этого списка. Тогда удаление O(1), доступ O(1), создание O(1) (кроме случая, когда массиву надо вдруг realloc сделать).

monk ★★★★★
()
Ответ на: комментарий от monk

В принципе, и realloc ничего не испортит, если мы в качестве адреса используем индекс, а не указатель, а массив только растёт. При этом погибший юнит не обязательно может исчезать навсегда. Просто его в его структуре будет признак живой/погибший. Это можно обыграть, если в игре предусмотрено воскрешение, например. Ну или просто он будет оставаться на поле боя.

Kogrom
()
Ответ на: комментарий от Kogrom

В принципе, и realloc ничего не испортит, если мы в качестве адреса используем индекс, а не указатель

Я имел в виду, что realloc испортит O(1).

Просто его в его структуре будет признак живой/погибший. Это можно обыграть, если в игре предусмотрено воскрешение, например. Ну или просто он будет оставаться на поле боя.

Тогда у нас бесконечно растущий массив. Это не очень хорошо.

В принципе, можно сразу создавать массив с максимально возможным количеством юнитов в игре.

monk ★★★★★
()
Ответ на: комментарий от monk

Тогда у нас бесконечно растущий массив. Это не очень хорошо.

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

Kogrom
()
Ответ на: комментарий от Kogrom

Так если мёртвых не удалять, то рост количества может достигать несколько десятков в секунду. А в некоторых современных, где отслеживается каждый воин в отряде, то и несколько тысяч в секунду легко сделать.

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

Я больше не про память, а про алгоритмы с квадратичной сложностью от количества (всякие проверки на видит/атакует/сталкивается/…). Если массив с юнитами ограничен, то можно использовать простой квадратичный. Если же постоянно растущий, то придётся делать всякие индексы от координат (которые тоже надо не забывать удалять вовремя).

monk ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.