company_banner

Как мы внедряли WebAssembly в Яндекс.Картах и почему оставили JavaScript

    Меня зовут Валерий Шавель, я из команды разработки векторного движка Яндекс.Карт. Недавно мы внедряли в движок технологию WebAssembly. Ниже я расскажу, почему мы её выбрали, какие результаты получили и как вы можете использовать эту технологию в своём проекте.



    В Яндекс.Картах векторная карта состоит из кусочков, называемых тайлами. Фактически тайл — индексированная область карты. Карта векторная, поэтому в каждом тайле содержится немало геометрических примитивов. Тайлы приходят с сервера на клиент закодированными, и перед отображением необходимо обработать все примитивы. Иногда это занимает существенное время. Тайл может содержать больше 2 тысяч ломаных и многоугольников.



    При обработке примитивов важнее всего производительность. Если тайл не будет подготовлен достаточно быстро — то и пользователь увидит его поздно, и следующие тайлы задержатся в очереди. Чтобы ускорить обработку, мы решили попробовать относительно новую технологию WebAssembly (Wasm).

    Использование WebAssembly в картах


    Сейчас бо́льшая часть обработки примитивов происходит в фоновом потоке (Web Worker), который живёт отдельной жизнью. Это сделано для того, чтобы максимально разгрузить главный поток. Таким образом, когда код для показа карты встраивается на страницу сервиса, который может сам добавлять существенную нагрузку, тормозов будет меньше. Минус в том, что нужно правильно настраивать обмен сообщениями между главным потоком и Web Worker’ом.

    Часть обработки, происходящая в фоновом потоке, состоит, по сути, из двух шагов:

    1. Декодируется формат protobuf, который приходит с сервера.
    2. Геометрии генерируются и записываются в буферы.

    На втором шаге формируются вершинный и индексный буферы для WebGL. Эти буферы применяются при рендеринге следующим образом. Вершинный буфер содержит для каждой вершины её параметры, которые необходимы для определения её положения на экране в конкретный момент. Индексный буфер состоит из троек индексов. Каждая тройка означает, что на экране должен быть отображён треугольник с вершинами из вершинного буфера под указанными индексами. Поэтому примитив необходимо разбить на треугольники, что тоже может быть трудоёмкой задачей:



    Очевидно, что во время второго шага происходит немало манипуляций с памятью и математических расчётов, потому что для правильного рендеринга примитивов нужно много информации о каждой вершине примитива:



    Нас не устраивала производительность нашего кода на JavaScript’е. В это время все стали писать о WebAssembly, технология постоянно развивалась и улучшалась. Почитав исследования, мы предположили, что Wasm может ускорить наши операции. Хотя мы не были полностью в этом уверены: оказалось сложно найти данные о применении Wasm’а в столь большом проекте.

    Также Wasm кое в чём очевидно хуже, чем TypeScript:

    1. Нужно инициализировать специальный модуль с необходимыми нам функциями. Это может привести к задержке перед началом работы этой функциональности.
    2. Намного больший, чем в TS’е, размер исходного кода при компиляции Wasm’а.
    3. Разработчикам приходится поддерживать альтернативный вариант исполнения кода, который к тому же написан на нетипичном для фронтенда языке.

    Однако, несмотря на всё это, мы рискнули переписать часть своего кода с использованием Wasm’а.

    Общая информация о WebAssembly




    Wasm — это бинарный формат; можно скомпилировать в него разные языки, а затем запустить код в браузере. Часто такой заранее скомпилированный код оказывается быстрее классического JavaScript’a. Код в формате WebAssembly не имеет доступа к DOM-элементам страницы и, как правило, применяется для выполнения на клиенте трудоёмких вычислительных задач.

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

    Для компиляции C++ в WebAssembly мы применяли emscripten. После его установки и добавления в проект на C++, чтобы получить модуль, нужно написать главный файл проекта определённым образом. Например, он может выглядеть вот так:

    #include <emscripten/bind.h>
    #include <emscripten.h>
    #include <math.h>
    
    struct Point {
        double x;
        double y;
    };
    
    double sqr(double x) {
        return x * x;
    }
    
    EMSCRIPTEN_BINDINGS(my_value_example) {
        emscripten::value_object<Point>("Point")
            .field("x", &Point::x)
            .field("y", &Point::y)
        ;
    
        emscripten::register_vector<Point>("vector<Point>");
    
        emscripten::function("distance", emscripten::optional_override(
            [](Point point1, Point point2) {
                return sqrt(sqr(point1.x - point2.x) + sqr(point1.y - point2.y)) ;
        }));
    }

    Далее я опишу, как вы можете использовать этот код в своём проекте на TypeScript.

    В коде мы определяем структуру Point и ставим ей в соответствие интерфейс Point в TypeScript, в котором будет два поля — x и y, они соответствуют полям структуры.

    Далее, если мы захотим вернуть стандартный контейнер vector из C++ в TypeScript, то понадобится зарегистрировать его для типа Point. Тогда в TypeScript ему будет соответствовать интерфейс с нужными функциями.

    И наконец, в коде показано, как зарегистрировать свою функцию, чтобы вызвать её из TypeScript по соответствующему имени.

    Скомпилируйте файл с помощью emscripten и добавьте получившийся модуль в свой проект на TypeScript’е. Теперь мы можем для произвольного модуля emscripten написать общий файл d.ts, в котором заранее определены полезные функции и типы:

    declare module "emscripten_module" {
        interface EmscriptenModule {
            readonly wasmMemory: WebAssembly.Memory;
            readonly HEAPU8: Uint8Array;
            readonly HEAPF64: Float64Array;
    
            locateFile: (path: string) => string;
            onRuntimeInitialized: () => void;
            _malloc: (size: size_t) => uintptr_t;
            _free: (addr: size_t) => uintptr_t;
        }
    
        export default EmscriptenModule;
        export type uintptr_t = number;
        export type size_t = number;
    }

    И можем написать файл d.ts для нашего модуля:

    declare module "emscripten_point" {
        import EmscriptenModule, {uintptr_t, size_t} from 'emscripten_module';
    
        interface NativeObject {
            delete: () => void;
        }
    
        interface Vector<T> extends NativeObject {
            get(index: number): T;
            size(): number;
        }
    
        interface Point {
            readonly x: number;
            readonly y: number;
        }
    
        interface PointModule extends EmscriptenModule {
            distance: (point1: Point, point2: Point) => number;
        }
    
        type PointModuleUninitialized = Partial<PointModule>;
    
        export default function createModuleApi(Module: Partial<PointModule>): PointModule;
    }

    Теперь мы можем написать функцию, которая создаст Promise на инициализацию модуля, и воспользоваться ей:

    import EmscriptenModule from 'emscripten_module';
    import createPointModuleApi, {PointModule} from 'emscripten_point';
    import * as pointModule from 'emscripten_point.wasm';
    
    /**
     * Promisifies initialization of emscripten module.
     *
     * @param moduleUrl URL to wasm file, it could be encoded data URL.
     * @param moduleInitializer Escripten module factory,
     *        see https://emscripten.org/docs/compiling/WebAssembly.html#compiler-output.
     */
    export default function initEmscriptenModule<ModuleT extends EmscriptenModule>(
        moduleUrl: string,
        moduleInitializer: (module: Partial<EmscriptenModule>) => ModuleT
    ): Promise<ModuleT> {
        return new Promise((resolve) => {
            const module = moduleInitializer({
                locateFile: () => moduleUrl,
                onRuntimeInitialized: function (): void {
                    // module itself is thenable, to prevent infinite promise resolution
                    delete (<any>module).then;
    
                    resolve(module);
                }
            });
        });
    }
    
    const initialization = initEmscriptenModule(
        'data:application/wasm;base64,' + pointModule,
        createPointModuleApi
    );

    Теперь по этому Promise мы получаем наш модуль вместе с функцией distance.

    К сожалению, отлаживать Wasm-код построчно в браузере нельзя. Поэтому необходимо писать тесты и запускать на них код как обычный C++, тогда у вас будет возможность удобной отладки. Тем не менее даже в браузере у вас есть доступ к стандартному потоку cout, который выведет всё в консоль браузера.

    По этой ссылке доступен проект-пример из статьи, там вы можете посмотреть настройку webpack.config и CMakeLists.

    Результаты


    Итак, мы переписали часть своего кода и запустили эксперимент, чтобы рассмотреть парсинг ломаных и многоугольников. На диаграмме продемонстрированы медианные результаты по одному тайлу для Wasm’а и JavaScript’а:



    В итоге мы получили такие относительные коэффициенты по каждой метрике:



    Как видно по чистому времени парсинга примитивов и времени декодирования тайла, Wasm быстрее более чем в четыре раза. Если же смотреть общее время парсинга, то здесь разница тоже значительная, однако она всё-таки немного меньше. Это связано с затратами на то, чтобы передать данные в Wasm и забрать результат. Также стоит отметить, что на первых тайлах общий выигрыш очень высок (на первых десяти — больше чем в пять раз). Однако потом относительный коэффициент уменьшается примерно до трёх.

    В итоге всё это вместе помогло на 20–25% снизить время обработки одного тайла в фоновом потоке. Конечно, эта разница не так велика, как предыдущие, однако нужно понимать, что парсинг ломаных и многоугольников — это далеко не вся обработка тайла.

    Если говорить о необходимости инициализации модуля, то из-за неё примерно у половины пользователей произошла задержка перед парсингом первого тайла. Медиана задержки — 188 мс. Задержка случается только перед первым тайлом, а выигрыш в парсинге постоянный, так что можно смириться с небольшой паузой на старте и не считать её серьёзной проблемой.

    Ещё одна отрицательная сторона — размер файла с исходным кодом. Сжатый gzip’ом минифицированный код всего векторного движка карты без Wasm’а — 85 КБ, с Wasm’ом — 191 КБ. При этом в Wasm’е реализован только парсинг ломаных и прямоугольников, а не всех примитивов, которые могут быть в тайле. Более того, для декодирования protobuf’а пришлось выбрать реализацию библиотеки на чистом C, с реализацией на C++ размер был ещё больше. Эту разницу можно несколько уменьшить, если при компиляции C++ использовать флаг компилятора -Oz вместо -O3, но она по-прежнему существенна. Кроме того, при такой замене мы теряем в производительности.

    Тем не менее размер исходника на скорость инициализации карты повлиял незначительно. Wasm хуже только на медленных устройствах, и разница — менее 2%. А вот изначальный видимый набор векторных тайлов в реализации с Wasm’ом был показан пользователям немного быстрее, чем с JS’ной реализацией. Это связано с бо́льшим выигрышем на первых обработанных тайлах, пока JS ещё не оптимизирован.

    Таким образом, Wasm сейчас — вполне достойный вариант, если вас не устраивает производительность кода на JavaScript. В то же время вы можете получить меньший выигрыш в производительности, чем мы, либо не получить его вообще. Это связано с тем, что иногда JavaScript сам работает достаточно быстро, а в Wasm требуется передавать данные и забирать результат.

    В наших картах сейчас работает обычный JavaScript. Это связано с тем, что выигрыш в парсинге не так велик на общем фоне, и с тем, что в Wasm’е реализован парсинг только некоторых типов примитивов. Если это изменится — возможно, мы станем применять Wasm. Ещё один весомый аргумент против — сложность сборки и отладки: поддерживать проект на двух языках имеет смысл только тогда, когда выигрыш в производительности того стоит.
    Яндекс
    630,28
    Как мы делаем Яндекс
    Поделиться публикацией

    Комментарии 48

      +2
      А почему в Яндекс картах не используется SVG?
      Браузер сам отлично справляется с прорисовкой/парсингом/обработкой контуров (возможно даже рисуется с аппаратным ускорением).
      SVG лучше подходит для печати с высоким DPI, чем растровые картинки сейчас.
        +11
        Не знаю за Яндекс, отвечу за свой опыт. SVG только в теории красиво, на практике же оборачивается адовой потерей производительности браузеров по итогу жуткими тормозами.
          0
          Вот это как раз было наиболее интересно — сравнение производительности.
          Раньше SVG никак не использовал GPU, поэтому WebGL был в явном выигрыше, но возможно сейчас ситуация изменилась в лучшую сторону.
          Кроме того — у Яндекса есть свой браузер, на котором можно «оптимизировать» SVG для своих-же карт :)
            0
            возможно сейчас ситуация изменилась в лучшую сторону
            У FF ситуация изменилась так, что вообще всё рендерится на GPU в Webrender, ЕМНИП.
              0
              > у Яндекса есть свой браузер, на котором можно «оптимизировать» SVG
              Для этого им придется делать полноценный форк хромиума и уже в нём переписывать отрисовку SVG, что, наверное, очень непростое решение, так как потом этот форк нужно будет самим поддерживать и развивать, а не просто обновять хромиум. Да и если ни у кого среди браузерных движков не получается реализовать быструю поддержку SVG, то может это в принципе невозможно, используя спецификацию SVG.
                0
                форк хромиума

                Хромиум это же интерфейс, и форк как раз хромиума у них есть. А с SVG — есть подозрение что потребовался бы форк уже Blink'а, а это уже заметно сложнее, учитывая что даже МС с их ресурсами отказалась от собственных движков.
                  0
                  Хромиум это не только интерфейс. Он включает в себя интерфейс, движок Blink, и JS-движок V8.
                    0
                    В любом случае — протолкнуть что-то, особенно такое крупное как изменения работы с SVG в движок браузера на порядок сложнее, чем небольшие багфиксы или улучшения не идущие вразрез с гугловским видением будущего проекта Chromium.
                    0
                    За графику в хромиуме отвечает библиотека Skia. И в FF кстати кажется тоже она. Библиотека сложная и небезглючная. Подозреваю, что форкать её — смерти подобно.
                    Я как-то репортил в хромиум баг, как раз-таки связанный с SVG — мне ответили, что в настоящий момент исправить его не представляется возможным, потому что это Skia, и там слишком всё сложно и слишком на многое завязано.
                    0

                    У них и так форк.
                    И они и так наделали уже много таких решений, которые не принимают (и врядли вообще когда-нибудь примут). Об этом говорилось тут на конференции яндекса: https://youtu.be/iWPu3Crpys0?t=1836

                      +1

                      Кому лень смотреть видео: толстые фичи (гибернейт, тайлы) гугл не принимает, багфиксы принимают на ура, фичу с выделением ссылок через Alt таки удалось протолкнуть.

                        0

                        Ааа, оно существует! Выделение ссылок! Спасибо, добрый человек, а то я мучаюсь!

                        0
                        Если у них форк, зачем им нужно подтверждение гугла, чтобы принять какую-то фичу?
                          +2

                          По-моему, очевидно, что яндекс не хочет все новые фичи и стандарты css/html/js внедрять сам, когда это может сделать гугл. При этом, чем больше яндекс сделает вещей, которые гугл не примет, тем сложнее яндексу будет принять себе новый код, который написал гугл.
                          Об этом тоже говорилось в докладе: они мержат себе изменения из хромиума без остановки — как только закончился один цикл, сразу начинается следующий

                            0
                            Об этом я как раз и говорил в моем первом сообщении:
                            > Для этого им придется делать полноценный форк хромиума и уже в нём переписывать отрисовку SVG, что, наверное, очень непростое решение, так как потом этот форк нужно будет самим поддерживать и развивать
                              0

                              А сейчас у них форк или он не "полноценный"? Или они не поддерживают и не развивают свой форк? Они как минимум для хибенейта сделали столько изменений в ядре, что их никогда не примут и им придётся с этим жить уже всегда.
                              SVG — просто ещё одно такое изменение, которое не понятно, окупится ли.

                                0
                                Если они постоянно мержат оригинальный Хромиум назад к себе, то не «полноценный», я в этом смысле имел в виду. А чтобы переписать SVG, им нужно будет переписать очень много всего в оригинальном и вряд ли уже потом полноценный мерж будет возможен.
                                > SVG — просто ещё одно такое изменение, которое не понятно, окупится ли.
                                Согласен.
                                  0
                                  Полноценный форк — это, например, когда появился WebKit на основе KHTML, или Blink как форк WebKit. Т.е. когда они в результате пошли своим путем.
                                    0

                                    Звучит как "настоящий мужчина — это"
                                    Не надо придумывать свою терминологию. Яндекс браузер — это форк хромиума. То, что он (пока?) может обновляться вместе с изменениями в хромиуме, не делает его "не форком" или менее "полноценным".

                                      0
                                      Я не придумывал свою терминологию, а выразил мысль. То, что вам это не понравилось, это, похоже, только ваше дело.
                                        –1
                                        Ответвлённый проект или форк может поддерживать и обмениваться частью содержимого с основным проектом, а может и приобрести абсолютно другие свойства, перестав иметь с базовым проектом что-то общее.

                                        Дальнейшее развитие может происходить разными путями: сосуществование и активный обмен общим (разделяемым) кодом, независимое существование, независимое существование с полной потерей общих свойств, «миграция» разработчиков из исходной ветки в другую, адаптация проекта к новым технологиям или слияние ответвлений в единый проект.

                                        Из вики. Так что то, что яндекс забирает обновления и при этом пытается свои изменения протолкнуть в основной проект — не делают его не форком. Так же кстати и другие крупные разработчики браузеров поступают — Opera, Vivaldi, Microsoft.
                                          –1
                                          Поэтому я и попытался как-то отделить исходное значение слова «форк», которое описывает сразу несколько сценариев, от того, которое я имел в виду, и написал «полноценный форк», предполагая эту часть определения из вики:
                                          а может и приобрести абсолютно другие свойства, перестав иметь с базовым проектом что-то общее

                                          В моем представлении, написать собственную обработку SVG — это настолько сложная задача, затрагивающая столько различных частей основного проекта, которая потом не позволит принимать большую часть обновлений с основного проекта.
                    +1

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

                      0
                      Полная спецификация довольно сложна для оптимизации, но для некоторого подмножества SVG, например без кривых она возможна, статья тут яркий пример кстати.
                      +2
                      Было бы очень круто, будь SVG таким быстрым. В картах динамические (и не очень) объекты на картах сделаны через SVG: маршруты, линейка, полигоны (когда ищете Москву, например). Так вот, браузер довольно плохо справляется с прорисовкой/парсингом/обработкой контуров и на объемах данных подложки будет выдавать 1-2 фпс в лучшем случае.

                      Попробуйте найти хотя бы один карточный движок, который использует SVG для подложки. Их нет. Все рисуют или дом-тайлами, или в canvas 2d, или в webgl.
                        +2
                        И это проблема, поскольку сами производители браузеров не горят желанием оптимизировать SVG, переписав его на какой-нибудь Vulkan ввиду того, что производительность в SVG не критична — карточные движки же его не используют :)

                        Было бы очень круто, если бы Яндекс-картам не приходилось писать свой векторный движок на JavaScript/WebAssembly + WebGL, а браузер нормально работал с вектором из коробки. Но «обход» этой проблемы никак не приближает её решение.
                        +1
                        SVG парсится в DOM, со всем вытекающим из этого оверхедом. С одной стороны, у SVG-элементов имеется доступ к CSS и DOM API, что, безусловно, удобно. К тому-же отрисовка SVG, во многих случаях, обходится браузеру гораздо дешевле, чем отрисовка обычных DOM-элементов: каждый элемент рисуется независимо, без сложных комбинаторных вычислений. С другой стороны, Canvas — гораздо быстрее, потому что не создает вообще никакой сложной объектной модели.
                          0
                          Но объектная модель, необходимая для карт от этого никуда не пропадает, просто ей занимаются в JavaScript, отвязавшись от DOM, по сути дублируя часть функций.
                            0
                            DOM избыточен и сам по себе медленный. В JS нужно наоборот минимизировать взаимодействие с ним.
                        +3

                        И все таки, почему именно плюсы, а не раст, у которого более развитая экосистема для wasm?

                          +10
                          Внутри Яндекса плюсы сильно более распространены, чем Rust, поэтому так. Проще было найти коллег, которые могли поревьюить код
                          0

                          Вполне коррелирует с нашим опытом — приходилось писать компилятор в wasm из некоего typescript-подобного языка, в итоге выигрыша в производительности получить не удалось — на маленьких проектах многое съедает интероп, на больших управление памятью в javascript оказывается на порядок лучше того, что "на коленке" можно сделать в wasm. Есть пропоузалы для решения этой проблемы, но вроде пока никто не реализовал. И плюс сама wasm-машина очень слабо оптимизирована, как кажется.

                            0
                            не смотрели на assemblyscript?
                              +4

                              Если у вас уже был ts код, тогда могли хотя б в сторону assemblyscript посмотреть. Тогда и размер был бы меньше. А emscripten запихал вам кучу всего ненужного, вот размер и раздулся.

                                0
                                Да, верно, можно было, мы даже немного посмотрели, но проблема в том, что TS (произносится как JS) — это другая среда, там есть свои примочки (например асинхронщина или генераторы), которые не сразу понятно как будут выражены в wasm, а с ним мы экспериментировали в первую очередь из-за производительности, там нужен полный контроль. Да и в момент когда мы начинали первые эксперименты (года 1.5 назад) он показался сыроватым.
                                Что касается размера, то мы, конечно, постарались в этом месте пооптимизировать, поотключать ненужное, но, опять же, среда там другая, понятия стандартной библиотеки или подобного там нет. И елси, например, мы используем где-то функцию sort(), то это значит что ее код попадет в модуль. В теории можно было бы, наверное, «сходить в js», но непонятно чего это будет стоить в рантайме из-за передачи данных и смены контекста.
                                +4
                                Забавно. Заголовок по сути говорит «wasm нам не понравился», а в статье его нахваливают.

                                В наших картах сейчас работает обычный JavaScript. Это связано с тем, что выигрыш в парсинге не так велик на общем фоне, и с тем, что в Wasm’е реализован парсинг только некоторых типов примитивов.

                                То есть, получается, что причины скорее внутренние — «когда мы реализуем в „Wasm“ больше из того что нам нужно, то вернёмся к этому вопросу».
                                Ещё ожидал увидеть то что вы пробовали отключить ключами Emscripten часть неиспользуемых вещей — например, RTTI или виртуальную файловую систему — это помогло бы уменьшить размер wasm-файла.
                                  –7
                                  Похоже, что WebAssembly — очередной хайповый оверхед, который так любят байтофилы. Затраты на скорость разработки и штат разработчиков на WebAssembly не покроют ничтожный выигрыш в производительности.
                                    +2

                                    Судя по статье, выигрыш в разы. Разве что не в десять раз, а всего-то в два. А то, что они переписали только небольшую часть, поэтому на общем фоне эти два раза не очень заметны, разве в этом есть вина wasm?


                                    А скорость разработки на rust не настолько ниже, чем на JavaScript, а иногда может быть и выше. В выборе с++ в данном случае wasm тоже как бы не виноват.

                                    0
                                    По-моему, вышла слишком большая разница. Скорее всего, код на JS был изначально написан неоптимально. Или это за счёт использования AVX?
                                      0

                                      Потренировались использовать WASM да и ещё зарплату выдали. Это же классно! Я бы тоже не отказался от такой работы. Возмите к себе. Научу делать трансформеры на канвасе.

                                        +2
                                        почему нет, начать можно отсюда yandex.ru/jobs
                                          –2

                                          Нет. Длинный путь.

                                        0
                                        Интересно, не рассматривали ли Rust как альтернативу C++? У него там вроде все очень хорошо с WASM, любопытно было бы узнать мотивацию.
                                        +1
                                        К сожалению, отлаживать Wasm-код построчно в браузере нельзя

                                        image
                                          0
                                          Ну вот не очень удобно получается, переменные например странные, инструментарий все же не совсем готов (по крайней мере в сравнении с другими развитыми технологиями, где сборка и вся разработка в два клика настраивается)
                                          Как в статье написано, делали так: плюсовый код запускался через тесты в нативной среде, где и дебаггеры крутые и по скорости приемлемо, а уже на финальном этапе тестировался в браузере. Сюрпризов не случалось, если что-то разному работало, то в итоге находилась и сама разница и (часто, чего уж греха таить) человеческий косяк.
                                          0
                                          Я правильно понял, что:
                                          1. Не внедрили, поскольку переписали на нём мало кода и прирост производительности вышел не большой.
                                          2. Больше кода не переписывали, поскольку не внедрили.

                                          ?
                                            0
                                            Не совсем так
                                            У нас получилось подтянуть средствами js производительность до нужного нам качества (параллельно копали во всех направлениях, кто-то делал эксперименты с WASM, кто-то оптимизировал то что есть).
                                            Сейчас основные проблемы сервиса в другом и в моменте нам кажется разумным переключиться на продуктовые задачи
                                            Если когда-то мы снова упремся в производительность, то с большой вероятностью воспользуемся изученным способом получать выигрыш в перфомансе
                                            Сейчас это просто не очень болит, а цена решения высокая

                                            Грубо говоря — проверили все грабли, васм норм, если понадобится добыть еще 20% к производительности — вот готовый способ

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

                                          Самое читаемое