Храним 300 миллионов объектов в CLR процессе

    Камень преткновения — GC


    Все managed языки такие как Java или C# имеют один существенный недостаток — безусловное автоматическое управление паматью. Казалось бы, именно это и является преимуществом managed языков. Помните, как мы барахтались с dandling-указателями, не понимая, куда утекают драгоценные 10KB в час, заставляя рестартать наш любимый сервер раз в сутки? Конечно, Java и C# (и иже с ними) на первый взгляд разруливают ситуацию в 99% случаев.

    Так-то оно так, только вот есть одна проблемка: как быть с большим кол-вом объектов, ведь в том же .Net никакой магии нет. CLR должен сканировать огромный set объектов и их взаимных ссылок. Это проблема частично решается путём введения поколений. Исходя из того, что большинство объектов живёт недолго, мы высвобождаем их быстрее и поэтому не надо каждый раз ходить по всем объектам хипа.

    Но проблема всё равно есть в тех случаях, когда объекты должны жить долго. Например, кэш. В нём должны находиться миллионы объектов. Особенно, учитывая возрастание объемов оперативки на типичном современном серваке. Получается, что в кэше потенциально можно хранить сотни миллионов бизнес-объектов (например, Person с дюжиной полей) на машине с 64GB памяти.

    Однако на практике это сделать не удаётся. Как только мы добавляем первые 10 миллионов объектов и они “устаревают” из первого поколения во второе, то очередной полный GC-scan “завешивает” процесс на 8-12 секунд, причём эта пауза неизбежна, т.е. мы уже находимся в режиме background server GC и это только время “stop-the-world”. Это приводит к тому, что серверная апликуха просто “умирает” на 10 секунд. Более того, предсказать момент “клинической смерти” практически невозможно.
    Что же делать? Не хранить много объектов долго?

    Зачем


    Но мне НУЖНО хранить очень много объектов долго в конкретной задаче. Вот например, я храню network из 200 миллионов улиц и их взаимосвязей. После загрузки из flat файла моё приложение должно просчитать коэффициенты вероятностей. Это занимает время. Поэтому я это делаю сразу по мере загрузки данных с диска в память. После этого мне нужно иметь object-graph, который уже прекалькулирован и готов “к труду и обороне”. Короче, мне нужно хранить резидентно около 48GB данных в течении нескольких недель при этом отвечаю на сотни запросов в секунду.

    Вот другая задача. Кэширование социальных данных, которых скапливаются сотни миллионов за 2-3 недели, а обслуживать необходимо десятки тысяч read-запросов в секунду.

    Как


    Вот мы и решили сделать свой memory manager и назвали его “Pile” (куча). Ибо никак не обойти “калечность” managed memory model. Unmanaged memory ничем не спасает. Доступ к ней занимает время на проверки, которые “убивают” скорость и усложняют весь дизайн. Ни .Net, ни Java не умееют работать в “нормальном” режиме с кусками памяти, которые не на хипе.

    Что мы сделали? Наш memory manager — это абсолютно 100% managed код. Мы динамически выделяем массивы byte, которые мы называем сегментами. Внутри сегмента у нас есть указатель — обычный int. И вот мы получаем PilePointer:
    /// <summary>
    /// Represents a pointer to the pile object (object stored in a pile).
    /// The reference may be local or distributed in which case the NodeID is>=0.
    /// Distributed pointers are very useful for organizing piles of objects distributed among many servers, for example
    ///  for "Big Memory" implementations or large neural networks where nodes may inter-connect between servers.
    /// The CLR reference to the IPile is not a part of this struct for performance and practicality reasons, as
    ///  it is highly unlikely that there are going to be more than one instance of a pile in a process, however
    ///  should more than 1 pile be allocated than this pointer would need to be wrapped in some other structure along
    ///   with source IPile reference
    /// </summary>
     public struct PilePointer : IEquatable<PilePointer>
     {
        /// <summary>
        /// Distributed Node ID. The local pile sets this to -1 rendering this pointer as !DistributedValid
        /// </summary>
        public readonly int NodeID;
    
        /// <summary>
        /// Segment # within pile
        /// </summary>
        public readonly int Segment;
    
        /// <summary>
        /// Address within the segment
        /// </summary>
        public readonly int Address;
    …………………………………………………………………
    }
    
    

    Обратите внимание на NodeID, о нём ниже. Получить PilePointer можно следующим образом:

    var obj = new MyBusinessType();
    var pilePointer = Pile.Put(obj);
    …………………………………………
    // где-то в другом месте, где есть поинтер
    var originalObj = Pile.Get(pilePointer);
    
    

    Мы получим копию оригинального объекта, который мы погрузили в Pile с помощью Put(), либо PileAccessViolation, если pointer неправильный.

    Pile.Delete(pilePointer) 
    

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

    Вопрос: как это сделано и что мы храним в byte[], ведь мы не можем хранить объекты CLR с реальными поинтерами, тогда они запутывают GC. Нам как раз надо обратное — хранить что-то в нашем формате, убрав managed references. Таким образом, мы сможем хранить данные, а GC не будет знать, что это объекты, ну и не будет их визитировать. Это возможно сделать через сериализацию. Конечно, имеются в виду не встроенные сериализаторы .Net (такие как BinaryFormatter), а наши родные в NFX.

    PilePointer.NodeID позволяет “размазывать” данные по распределённым “кучам”, так как он идентифицирует узел в когорте distributed pile.

    А теперь главный вопрос. Зачем это всё надо, если “под капотом” используется сериализация и это медленно?

    Скорость


    Реально это работает так: объект < 300 байт, погруженный в byte[] с помошью NFX Slim сериализации, занимает в среднем меньше на 10-25% места чем native объект CLR в памяти. Для больших объектов эта разница стремится к нулю. Почему так получается? Дело в том, что NFX.Serialization.Slim.SlimSerializer использует UTF8 для строк и variable length integer encoding + не нужен 12+ байт CLR header. В итоге камнем преткновения становится скорость сериализатора. SlimSerializer “держит” феноменальную скорость. На одном ядре Intel I7 Sandy Bridge с частотой 3GHz мы превращаем 440 тысяч PilePointer’ов в объект в секунду. Каждый объект в этом тесте имеет 20 заполненных полей и занимает 208 байт памяти. Вставка объектов в Pile одним ядром 405 тысяч в секунду. Такая скорость достигается за счёт динамической компиляции expression trees для каждого сериализируемого объекта в pile-сегмент. В среднем SlimSerializer работает раз в 5 быстрее, чем BinaryFormatter, хотя для многих простых типов этот коэффициент достигает 10. С точки зрения спэйса SlimSerializer пакует данные в 1/4 — 1/10 того, что делает BinaryFormatter. Ну и самое главное. SlimSerializer НЕ ТРЕБУЕТ специальной разметки полей в объектах, с которыми мы работаем. Т.е. хранить можно всё, что угодно, кроме delegates.

    Многопоточный тест на вставку данных стабильно держит больше 1 миллиона транзакций в секунду на CoreI7 3GHz.
    Ну и теперь самое главное. Аллокировав 300.000.000 объектов в нашем процессе полный GC занимает менее 30 миллисекунд



    Итоги


    Технология NFX.ApplicationModel.Pile позволяет избежать непредсказуемых задержек, вызванных сборщиком мусора GC, держа сотни миллионов объектов резидентно в памяти в течении длительного времени (недели), обеспечивая скорость доступа выше, чем “out-of-process” решения (такие как MemCache, Redis et.al).

    Pile основан на специализированном memory manager’е, который аллокирует большие byte[] и распределяет память для приложения. Погруженный в Pile объект идентифицируется структурой PilePointer, ктр. занимает 12 байт, что способствует созданию эффективных object graphs, где объекты взаимно ссылаются.

    Get the code:

    NFX GitHub
    Метки:
    Поделиться публикацией
    Комментарии 81
    • +2
      А где код-то?
        • +1
          Я, видимо, попал в момент между публикацией статьи и заливкой исходников.
        • +3
          Бывает же кому-то не скучно O_o
          • 0
            Это приводит к тому, что серверная апликуха просто “умирает” на 10 секунд. Более того, предсказать момент “клинической смерти” практически невозможно.

            Вообще-то, можно. И в вашем случае, как раз и нужно.
            • +2
              Что с того, что им можно немного управлять и немного мониторить. Ну это ни как не устраняет неэффективность дефолтного сборщика мусора и менеджера памяти в целом для задач с большими данными.
              • +3
                Там как раз пример есть. Поднимается два инстанса приложения. GC паузы мониторятся. Как только на каком-то инстансе запускается сборка мусора, он исключается из балансировщика и спокойно себе собирает мусор пока запросы обрабатывает второй инстанс.
                • 0
                  у меня в много objects- они все volatile, етот state стрoился 2 недели, о каком переклучении процесса идет реч? реконструироват 500.000.000 objects for 2 weeks just to collect 50 Mb of trash?
                  • +1
                    Ну это же ужасно, в 2 раза больше памяти, а вдруг будет наложение, так что 3 раза хранить. А потом когда разлипнет процесс, то что, эту память нужно синхронизировать между процессами, она же изменится.
                    • +3
                      то что мы сделали основано исклучително на ПРАКТИЧЕСКОМ опыте.
                      Перемещать десятки миллионов записеи в раме очень медленно. Между машинами — нереално.
                      Я молчу о сотнях миллионов записеи. А если записи связани друг с другом — то ето вообше коллапс.
                      It takes DAYS to move billions of rows between nodes
                      • +1
                        Вот этот подход с несколькими процессами подходит только для кеша в stateless приложениях, когда кеш прост и пассивен, а работать с живой моделью предметной области, то переключение неизбежно порвет ее консистентность.
                        • 0
                          Да, простите, в вашем случае не разобрался. Просто глаз зацепился за невозможность сосуществования со сборками мусора.
                          • +9
                            Просто в статье написано кэш, а это подразумевает, что его можно потерять и восстановить. Тут скорее модель, основная копия объектов, где они живут как в бездисковой СУБД.
                          • +1
                            Перемещать десятки миллионов записеи в раме очень медленно. Между машинами — нереално.

                            Ну так вы сделайте через event sourcing с бродкастом событий между инстансами через какой-нибудь EventStore, будет постоянно живая полная реплика, проблему-то нашли.
                            • 0
                              Что вы имеете ввиду? Если я правильно понял, то можно пробрасывать данные между процессами IIS?
                              • 0
                                Вы так это описываете, как будто пишете такие системы по три раза перед завтраком.
                                • 0
                                  Ну так суть в том, что нет ничего сложного в поддержке синхронизированного состояния между инстансами в кластере, все необходимые инструменты давно доступны.
                                  • +1
                                    а как же моногофазовый пахос, или на многих нодах будет тогда мусор?
                                    вы поймите — локалный рам всегда быстрее чем network, даже 40 Гбит. + координация ИП ядра на послки пакетов, DMA, IRQ

                                    Загрузите код и сделайте тест
                            • 0
                              Горизонтальное масштабирование. Нагрузка делится равномерно на несколько инстансов. К тому же, получаем бОльшую отказоустойчивость (если какой-то инстанс выйдет из строя, его заменят другие). Аппаратные ресурсы при этом, обычно, разумная жертва в угоду производительности.
                              Синхронизация это уже отдельная задача архитектуры. Полезно почитать.
                              • +3
                                это ужe на 1м ноде. в кластере у нас сотни миллиардов объектов.
                                как раз это и позволиает имет каш хит rate > 25% очен дешево ЛОКАЛьНО
                        • +2
                          попробуите хранить устоичиво 500.000.000 objects таких:
                          person
                          {
                          GDID key, //this is 12 bytes
                          GDID user, //12 bytes
                          string msg,
                          DateTime ts,
                          GDID? ref //1 or 13 bytes
                          }
                          и посмотрите что будет с вашим сервером
                          • +4
                            Массивы структур использовать не пробовали? С указанием строк как оффсета в массиве char-ов. Получилось бы, внезапно, то же самое, но куда эффективнее как с точки зрения отсутствия расходов на сериализацию, так и в плане GC pressure (не нужно собирать десериализованные экземпляры).
                            • +2
                              а что если НЕИЗВЕСТНО какие типы данних надо хранить? например — в ДБ каше мы храним бизнесс данные — какои их тип — неизвестно.
                              это может быть листы сложных объектов, транзакции. Хранить все во флат byte[] — это значит не исползоват C# objects как таковои.
                              Pile — ето БАЗА ДАННИХ/warehouse of objects в памяти
                              • +2
                                Ну так сделайте обобщённые классы пулов на generic-типах, пока типы будут соответствовать набору правил (нет ссылок на reference-типы), всё будет работать на отлично.
                                • +3
                                  Кодогенерация в рантайме?
                                • 0
                                  А как работать с массивом char-ов? Имеется ввиду создавать объект string перед использованием?

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

                                  Еще не представляю как делать освобождение таких строк, если исходная структура больше не нужна, и ее можно удалить из массива или перезатереь другой, то как можно узнать, что больше никто не держит копию этой структуры? В сценарии «загрузить большой файл в память и с ним работать» это конечно неактуально, но наверняка найдутся другие сценарии. Разве что заботиться не об удалении единичных экземпяров, а об дропе некого большого блока «устаревшего кэша».
                                • +3
                                  А почему не хранить такое в какой-нибудь редиске или другой No-SQL? Зачем вообще нужно хранить много данных в процессе?
                                  • +2
                                    Если нужно выполнять алгоритм на большом толстом графе, то к вашей редиске нужно будет несколько десятков, а то и сотен обращений на запрос. Дороговато выходит.
                                    • 0
                                      Так для этих целей графовые СУБД есть. Хотя я понял о чём вы, это как раз о том случае, когда ты сам реализуешь СУБД.
                              • +18
                                Т.е. сначала одни люди прикрутили абстракцию, избавляющую от управления памятью, затем другие люди поверх прикрутили абстракцию, эмулирующую управление памятью…

                                не, я понимаю зачем это все, но чувство парадоксальности происходящего не оставляет.
                                • –3
                                  деиствительно смешно, но нам ето надо чтобы разгрузить сервера и кашать реално 500.000.000 комментариев в памяти.
                                  это очень нужная вешь — она работает в разы быстрее чем мемкаш out-of-process
                                  • +2
                                    Может, ваш выбор — C++? :-)
                                    PS. Черт, не увидел коммент Andrey2008 строкой ниже :(
                                • +15
                                  Философски. А ведь можно было просто использовать Си++. Собрать 64-битное приложение и вперёд. :)
                                  Это видимо тот интересный переходный момент, когда преимущества языка превращаются в ограничения, которые мучительно преодолеваются.
                                  • +1
                                    да можно было просто хранить объекты .net в unmanaged памяти и спокойненько к ним обращаться и они не были бы подхвачены GС — было бы желание…
                                    • –1
                                      I think you are missing the point. The point is to use EXISTING objects that exist for absolutely different purposes, i.e. business logic, not DTO.
                                      Why would I create 1000s of DTO objects in an unmanaged heap just to cache data. I already have 100s of business types with complex logic,
                                      some with visitor pattern and double-dispatch etc… (i.e. friend-friend) interaction.
                                      Pile allows me to use polymorphism and full features of C#, while not loading GC at all
                                      • +6
                                        Да какая разница что хранить? Хранятся только данные, методы никак к данным не относятся (не зря же туда this передают?)). Структура .net объектов в памяти ниразу не секретная — переместить данные в неуправляемую кучу и поднастроить ссылки дело нехитрое, более того это уже даже на конференциях показывают — например как шарить .net объекты (как объекты) между процессами в shared memory…
                                        • –1
                                          Первый раз об этом слышу, плз дайте что нибудь почитать или поглядеть…
                                          • +2
                                            На CLRium-ах было несколько раз. Единственное, код становится ни разу не переносимым, на Mono уже не запустить.
                                            • +3
                                              Скромно оставлю ссылку =) clrium.ru
                                            • –1
                                              Pile — это 100% managed solution, именно поэтому он переносим легко межды платформами и применим к любым бизнес типам.
                                              То что предлагаете вы — абсолютно не портабельно и делает невозможным работу с objects в стандартной парадигме работы в C#.
                                              How do you access a «property» of a byte buffer via a pointer? Of course its all the same processor opcodes at the end of the day.
                                              It would be easier to use C++ and forget about C# in your case.
                                              • 0
                                                How do you access a «property» of a byte buffer via a pointer? Of course its all the same processor opcodes at the end of the day.
                                                В том и суть, что весь остальной код думает, что это обычный объект и работает с ним по обычной же ссылке на объект.
                                        • 0
                                          Просто автор не знал про наличие в языке value-типов, специально для таких штук предназначенных.
                                          • 0
                                            а как хранить ссылки в inside value type? на что ссылатся? на другои валуе type? это как, ану расскажи?
                                            • 0
                                              Делаете структурку типа
                                              struct Link<T>
                                              {
                                                   int PoolId; //id массива
                                                   int ObjectId; //по факту индекс в массиве
                                                   //Тут всякая всячина с реализацией Equals, == и т. п.
                                              }
                                              
                                              её храните в своих «объектах» вместо настоящей ссылки и потом резолвите. К ней, соответственно, extension-метод
                                              static T Resolve<T>(this Link<T> link)
                                              {
                                              //тут обрабатываете логику пулов
                                              }
                                              


                                              В самом «объекте» храните ещё поле Self, чтобы на него можно было без геммороя ссылаться.

                                              Итого: из оверхеда дополнительно тратим 8 байт памяти на объект для Self, ссылки как были 8 байт, так и остались.
                                              • 0
                                                The Pile concept allows you to work with CLR Object of ANY STRUCTURE.То что вы предлагаете — это какои-то edge-case for some particular «object».
                                                • +15
                                                  >>ANY STRUCTURE
                                                  Ложь и провокация. Если я положу туда Stream со ссылкой на unmanaged-ресурс, то оно не будет сериализовано. И не edge-case, а просто другое представление структуры объекта.

                                                  И это, вы можете на каком-то одном языке писать? Раздражает.
                                                  • +3
                                                    Sorry, don' have ru-keyboard. my typing sucks.

                                                    You are absolutely correct. One can not put a stream with unmanaged handles in it.
                                                    But, like I have said, the concept is used for PRACTICAL cases which are as follows:
                                                    in tasks where one needs to store very many graph nodes resident in memory, AND retain the business purpose of every node,
                                                    instead of creation of DTOs that are only purposed for marshalling/storage, the Pile allows you to store «data with code».
                                                    It is a practical system, not a puristic one. So you are correct. you can't store delegates, unmanaged handles etc… but I have never needed it

                                                    You store in pile what you would have stored in Redis(ore memcache). Only faster. Why? because the data is already here, and serialization is orders of magnitude faster that JSON->string->redis.

                                                    Pile: [«any» object] -> bin ser -> byte[] pile(already alloced)
                                                    Memcache/Redis: [«any object»] -> JSON ser/bin ser->network/TCP-> memcache/redis server

                                                    One would think that serialization is very slow, I thought this when we started the project. The reality is different,
                                                    — we have not even expected to achieve such a good PRACTICAL result.

                                                    I hope this makes more sense now.
                                                    • +2
                                                      Тут идея в том, что в случае хранения всего в value-типах накладываются примерно те же самые ограничения (можно хранить только POCO), но за счёт дружественной к GC структуры данных можно избежать расходов на сериализацию/десериализацию, ненужного GC pressure (value-типы размещаются на стеке, а не в куче) и т. д.
                                                      При этом сами данные могут быть любые. За счёт использования generic-типов можно построить инфраструктуру, которая будет обрабатывать любой тип соответствующий требованиям.
                                                      То есть, получаем те же самые преимущества, но ещё эффективнее.

                                                      Я бы соорудил proof-of-concept, но для нормального сравнения производительности надо писать свой аналог malloc, а это слишком лениво делать перед праздниками.

                                                      А так идея довольно стара и активно мной использовалась ещё в школьные времена для программирования на QuickBasic, где не было объектов и ссылок, только структуры и массивы
                                              • 0
                                                Ага, а потом когда выясняется что эти структуры надо уметь удалять, начинаются пляски с аллокациями, списками свободных индексов, отдельные треды для компактификации… и элегантные шорты превращаются в Neo4j! В лучшем случае. А в худшем — в ту же самую управляемую кучу, только свелосипеженную с нуля. Ой…
                                                • 0
                                                  Ну так предложенный автором Pile этих проблем не отменяет.
                                                  • 0
                                                    Да, не отменяет. Это, так, мысли в слух. Просто прикольно понимать, что managed языки, unmanaged языки и базы данных — это, на самом деле, отдельные области единого спектра.
                                                    • 0
                                                      На самом деле Pile и вышепредложенный способ имеют примерно ту же область применения, что и сверхбыстрый аллокатор, который не задумывается о необходимости освобождать память — когда она всегда освобождается одним единым куском на несколько гигабайт.
                                              • +1
                                                Звучит очень интересно, но идея до конца не ясна. Подскажите пожалуйста как это называется или ссылки на статьи или просто где можно про такое поподробнее почитать?
                                            • 0
                                              Если у вас даже сплошные value types все равно надо хранить строки. А строка, как ни крути — референс тип. Нy не быду же я свои строки делать?
                                              • 0
                                                Нy не быду же я свои строки делать?
                                                А в чём проблема? Делаете под них точно такой же пул, только состоящий из большого массива char-ов. И храните оффсет в нём и длинну.
                                                • –1
                                                  именно это и делает SlimSerializer, на котором стоит LocalPile, под капотом. Просто от вас ето скрыто и не нужно об этом думать когда пишеш бизнес логику
                                                  • 0
                                                    Скрытость под капотом даёт накладные расходы на сериализацию и GC-pressure. Вы с GC вроде как боролись, нет?
                                                    • –1
                                                      зачем вы тогда исползуете C#? C++ намного больше подходит для етих целей. статья была вообше не об этом.
                                                      мои GC расходы — это Gen0 а не Gen2 — вот и вся разница
                                                      • +1
                                                        зачем вы тогда исползуете C#?
                                                        Потому что он всеяден. Когда надо — можно аккуратно сделать такое вот эффективное с точки зрения GC решение. Ещё структуры очень активно используются в геймдеве (XNA, Unity3D), когда вообще нельзя создавать GC pressure.
                                          • +3
                                            blog.aumcode.com/2013/08/what-is-nfx-what-is-unistack.html
                                            NFX это УНИСТАК — pile is less than 1% его features, невозможно просто взять и хранить + обрабатывать чтото в C++ — там совсем другая модель всего — значит опят надо все переписивать тепер на C++: logging, cluster process control, configuration, UDDI, networking stack.
                                            В етом и весь смысл — не переписиват все осталное 25 раз. Обычно в таких случаях исползют мемкаш или редис — но ето намного менее еффективно чем держaть все прамо в рам. Например етот подход исползуется нами в граф-дб для нахождения друзеи.
                                            • –4
                                              Поменьше синтаксических ошибок пожалуйста, читать не приятно.
                                              Но тема интересна.
                                              • +4
                                                Вы бы сами делали поменьше ошибок, а не другим советовали.
                                              • –1
                                                «Не приятно» должно писаться слитно в вашем предложении.
                                              • 0
                                                Вы ранее упоминали, что у вас нет периодически клавы с русской раскладкой. А в данном случае она у вас то появлялась, то исчезала, что-ли? Не то, чтобы придираюсь, просто любопытно.
                                                • –3
                                                  не печатал по-русски где-то 23 года… заржавел
                                                  не пойму однако, почему это так важно для многих
                                                  • 0
                                                    Может быть, примерно потому что, если я влезу на Stackoverflow и начну писать на немецком, английском и французском одновременно, периодически разбавляя русским, то это будет выглядеть несколько странным? :) Понимаю, что аналогия несколько отличается, но частично совпадает :)
                                                    А вообще, с почином вас на Хабре! Пишите ещё. Хардкорные технические статьи всегда на пользу. Но, действительно, пожалуйста, если можно — поменьше сленга и выпендрёжа местами. Возможно, это потому что по-русски давно не писали. В общем, немного поскромнее, поподробнее и было бы вообще замечательно :)
                                            • –10
                                              Легче было переписать на языке без сборщика мусора
                                            • +3
                                              Кстати, очень похожим образом работал класс XPathDocument. Там после парсинга все XDM-дерево хранится именно вот так, отображением на большие массивы байт индексацией. И обход дерева через XPathIterator ходит по этим байтам (в которых хранятся и относительные сдвиги на соседние узлы по разным осям).
                                              • –1
                                                Сам пишу в основном на .NET. Но тут явно напрашивается С++. CPU Профайлер + _CRTDBG_MAP_ALLOC превращают кривонаписанные поделки в high-load ready приложения… И никаких больше «danGling» указателей
                                                • +4
                                                  Вы лихо за 10 минут переписываете гигантские проекты которые доросли до hiload на плюсы?
                                                  • –1
                                                    Я предпочитаю потратить больше времени на разработку архитектуры, чем на кодописание и фишечки. Если проект вырос в high load из наколенной поделки, значит изначально не было понимания требований, переписывание тут ни при чем.

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