вторник, 26 декабря 2017 г.

Про производительность PWA и single page приложений

         Меня давно посещала мысль что все эти PWA/single page application и переиспользование JS кода это утопия. И эта утопия достижима только если вы работаете над государственным проектом и вам насрать на performance budget. А как только у вас появляются реальные требования по производительности то выяснится что стратегии оптимизации client side JS и server side JS - совсем разные, можно сказать противоположные. 
         К примеру в этой статье  https://habrahabr.ru/post/345212/.com[perevod]-vy-mozhete-sebe-eto-pozvolit - наглядно доказано что написать хорошее PWA/single page application это очень сложно. Я давно подозревал что это именно так, что обеспечение быстрой первой загрузки - это пиздец какая проблема, но у меня не было данных чтобы это доказать. Теперь есть. 
         https://hpbn.co/ - High Performance Browser Networking by  Ilya Grigorik - надо будет обязательно прочитать.

понедельник, 25 декабря 2017 г.

Exceptions в Java

     Многие критикуют Golang за отсутсвие Exceptions. Якобы из-за этого пишется очень много кода по обработке ошибок. С одной стороны это правда, этот код писать нужно. Ошибка сама не всплывет вверх по стеку, поэтому как минимум if + return написать придется. Но с другой стороны этот подход оказывается оправданным для performance critical code. В этой статье - http://java-performance.info/throwing-an-exception-in-java-is-very-slow/  анализируется производительность Java Exceptions. Оказывается бросить исключение это пиздец как медленно  - 1-5 микросекунд. И для performance critical кода предлагается кэшировать исключения. По-моему это костыль хуже некуда. Либо возвращать кастомные объекты со строкой сообщение об ошибке внутри. Это тот же самый error в Golang. В общем когда смотришь на Java с опытом использования Golang - то все выглядит совсем не так радужно как это выглядело раньше. 

Коллекции в Java

      В своем недавнем посте я разнес в щепки стандартную реализацию HashMap в  Java. Оказывается не все так плохо. Не я один считаю что стандартный HashMap отстой, и Java коммьюнити написало альтернативные варианты реализации HashMap.  В http://java-performance.info/hashmap-overview-jdk-fastutil-goldman-sachs-hppc-koloboke-trove-january-2015/ проведено неплохое сравнение этих реализаций. Чемпионами оказались fast utils и  Колобок! В общем пошел читать исходники fast utils, буду учиться писать на Java у настоящих мужиков.

среда, 20 декабря 2017 г.

Golang sync.Map

      sync.Map в появившийся в Golang 1.9 оказался довольно занятной штуковиной. Я посмотрел видео с конференции:
 Перечитал еще раз слайды - https://github.com/gophercon/2017-talks/blob/master/lightningtalks/BryanCMills-AnOverviewOfSyncMap/An%20Overview%20of%20sync.Map.pdf но все равно не мог четко понять - в каких случаях стоит использовать sync.Map а в каких обычный map[] + mutex. Может недостаточно понятно написано, может просто я такой одаренный. В общем мне чтобы разобраться нужно залезть в исходники, что я собственно и сделал - https://golang.org/src/sync/map.go?s=822:2269#L16
          Итак под капотом sync.Map находятся две map[] - readonly map и dirty map. Readonly map используется для быстрого чтения/записи уже существующих значений. В readonly map храниться не само значение а указатель на него. Для чтений значения используется  atomic.LoadPointer(), для записи - atomic.CompareAndSwapPointer(). Это так называемый fastpath, то есть собственно для чего все и затевалось. И используем мы этот fastpath если пишем или читаем значения по уже существующим ключам. Все остальное - происходит уже не так быстро. В случае если мы вставляем новое значение - оно попадает в dirty map. Все операции с dity map происходят с заблокированным мютексом. После какого-то количества cache miss - попыток чтения новых значений из readonly map происходит промоушен dirty map до readonly map.  После этого все недавно добавленные ключи можно будет читать писать без блокировки. 
        Итого: читаем/пишем по существующим ключам очень быстро (сильно быстрее map + mutex),  потребляем в 2 раза больше памяти на указатели/хэш таблицы(dirty map также хранит все ключи из readonly map). Но размер значений тут никакой роли не играет, так как мы не храним значения а только указатели на них.  Добавление новых ключей и look up по вновь добавленным ключам происходит сильно медленнее обычной map + mutex потому что появляется дополнительный indirection level(указатели), и сами операции с ними - atomic, а это значит много memory barrier-ов.    

вторник, 19 декабря 2017 г.

Проникновение ИТ в быт

     В очередной раз убеждаюсь что по некоторым параметрам Китай уже далеко впереди планеты всей. Один из эти параметров - это проникновение ИТ в быт, в традиционные отрасли бизнеса не связанные с ИТ. Почему-то в Европе да и у нас - ИТ (за небольшим исключением) сосредоточено вокруг интернета и "около интернетных" областей деятельности. Такие области деятельности как ресторанный бизнес вообще  ИТ вообще не сильно затронуло - за исключением разве что кассового аппарата. Здесь в Ханчжоу во многих ресторанах на уголок стола приклеен QR код, отсканировав который в AliPay приложении  ты можешь сразу увидеть меню, сделать заказ, а когда покушаешь - и сразу оплатить его. Причем люди сидящие за одним столом могут видеть что заказывают другие, и один человек может оплатить за всех. В некоторых местах бумажного меню вообще нет, в принципе. А нахера оно если у каждого есть смартфон и AliPay ??
         Тоже самое с такси - все без исключения таксисты принимают AliPay. Многие даже не хотят с наличкой связываться вообще, требуют AliPay. У нас только ленивый не потешается над платежной системой МИР. А китайцы сделали себе UnionPay и в ус не дуют. Международные кредитные карточки мало где принимают. В основном только в гостиницах и аэропортах. Ну еще из банкомата можно наличку снять. А так - либо UnionPay, либо AliPay. 

Нанкин (Nanjing)

     На выходных довелось побывать в южной столице Китая - Нанкине. Этот город был столицей поднебесной до того как ее перенесли в северную столицу - Пекин. Сейчас Нанкин это средний по китайским меркам город, что-то около 8 миллионов житилей. В нем расположены довольно много достопримечательностей: гробницы времен династии Мин, мавзолей Сунь Ятсена, древние крепостные стены. Много парков и озер. Несмотря на размер оставляет впечатление довольно провинциального города. Не китайскую еду или кофейню найти очень проблематично. Зато женщины там заметно красивее тех что я наблюдаю в Ханчжоу или Шанхае.  


Разучились работать

      В последнее время я довольно тесно вовлечен в один из проектов над которым работают наши китайские коллеги из сами знаете какой компании. Невольно замечаю некоторые  "национальные особенности" работы. Работают ребята как проклятые. Никаких тебе разговоров про мотивацию, work-life баланс - такое впечатление что они вообще не знают что это такое. Такое впечатление что их команды вообще работают вопреки законам people management-а. Начальник прилюдно распекает подчиненного на чем свет стоит,   тот утирает сопли(образно) и еще шибче начинает работать. У нас стоит какому-нибуть менеджеру себе такое позволить - да его самого за такое уволят. И тот человек на которого накричали полгода после этого будет "демотивированным".  В общем разница в менталитетах - просто колоссальная. 
       Да,  очень много из того что они делают - они делают не оптимально, да сам результат зачастую выглядит как говно (костыль на костыле) - но они делают чтобы это гавно работало, и делают это в срок. И это просто невероятно. Потому что дедлайны изначально ставятся нереальные. И они умудряются в них укладываться. За счет нереальной самоотдачи. Когда в 9 вечера находят с десяток багов и к утру разработчик обещает все пофиксить.  Да я честно уже не вспомню когда я такое встречал в лазаде. У нас дедлайн по задачам в днях измеряются, и один день - это вообще минимальная единица измерения. А тут чувак обещает что все к утру исправит. Я уверен что он домой сегодня не попадет и что за овертайм ему никто не заплатит. 
       Плюс к этому строгая дисциплина, где дедлайн - это действительно  dead line и переступить его - смерти подобно. И дедлайны эти контролируются дважды в день, так что не забалуешь. В общем это производит впечатление такого бездушного комбайна который выжимает из людей все соки, но достигает цели. Не берусь судить - хорошо это или нет, но мне кажется мы уже разучились так работать.  И именно поэтому конкурировать с ними мы уже врятли сможем. 

понедельник, 18 декабря 2017 г.

Java HashMap vs Golang map[]

      Волею судьбы сейчас я плотно занимаюсь Java, вместо горячо любимого мною Golang. Ну и попутно сравниваю как решены те ил иные задачи в Golang и Java. Сегодня речь пойдет про хэш таблицы. С точки зрения алгоритмов HashMap и map[] практически идентичны - hash table + разрешение коллизий через цепочки. Но это теоретически, а с практической точки зрения реализация значит ничуть не меньше чем выбранные алгоритмы. Итак начнем: исходники HashMap я взял отсюда, а исходники map[] отсюда

Golang map[]

    1. Общее устройство. Данные хранятся в блоках по 8 элементов. Младшие 3 бита хэша используются для идентификации key-value пары внутри блока. Хэш таблица представляет собой массив из таких восьми-элементных блоков.  Первый блок из восьми элементов всегда располагается непосредственно в хэш таблице.  У этого есть важное следствие - Go map[] чувствует себя очень комфортно при количестве коллизий меньше 8 на элемент хэш таблицы. С учетом того что удлинение цепочки коллизий также происходит блоками по 8 элементов, то go map[] вообще весьма устойчива к росту числа коллизий.  Сам блок представляет из себя структуру из 8 однобайтовых значений для хранения младших битов хэшей, затем 8 ключей и 8 значений. В зависимости от типа ключей и значений они могут хранится непосредственно в этой структуре данных либо там могут хранится только указатели на них.
    2. Хэш функция - Go использует в качестве хэш функции встроенную в большинство процессоров функцию AES шифрование(инструкция AESENC). 
    3. Начальный размер хэш таблицы:   
           3.1 Для пустого map[] хэш таблица вообще не создается, 
           3.2 Если при инициализации map capacity указана меньше 8  - то хэш таблица также не создается.
          3.3 Если capacity указанная при создании map[] больше или равно 8, то размер таблицы рассчитывается как ближайшая степень двойки для которой выполняется условие (1 << B) * loadFactor > count, где B - степень двойки, loadFactor - магическое число равное 6.5, count - map[] capacity указанная при инициализации.  
    4. Расширение хэш таблицы. При расширении хэш таблицы учитывается два фактора:
        a. Количество элементов в map[], при этом логика аналогична описанной выше (как при инициализации map[])
        b. Количество цепочек длинной больше 8 элементов. Как я написал выше  Golang map[] оптимизирован для работы с цепочками длинной менее 8 элементов, поэтому реализация заточена на то чтобы сохранить длинну цепочки от одного до двух элементов. Так как счетчик "переполнений"(цепочек длинной более одного элемента) это uint16, то сама логика различается для map размером хэш таблицы меньше (1 << 16) и больше этого значения.
        если размер таблицы меньше чем (1 << 16) то хэш таблица расширяется в случае если количество цепочек с переполнением становится больше чем (1 << B) (где B - степень двойки, размер хэш таблицы). 
       если размер таблицы больше чем (1 << 16) то логика остается той же, но расчет количества "переполнений" становится приблизительным.
      Если принять во внимание то что в одном звене цепочки может находится до 8 элементов - то расширение хэш таблицы случается всегда по первому условию ( количество значений больше чем 6.5 * (1 << B)).  

Java HashMap 

1. Общее устройство HashMap предельно простое. Это классический hash map как он описан в учебниках по программированию. Хэш таблица представляет собой массив Entry, в каждом Entry ключ, значение и указатель на следующий Entry. Никаких хитростей и попыток оптимизировать что-то. Прям скуку смертная да и только.
2. Хэш функция - как я понял вызывается встроенный в объект метод hash() к которому сверху применяется простейшая XOR функция:
3. С начальным размером таблицы все тоже очень грустно - оно всегда равно 16 элементам. Либо ближайшей степени двойки большей чем требуемая capacity.
4. Расширение хэш таблицы реализовано также крайне просто - есть loadFactor, равный 0.75. Как только количество элементов в HashMap превышает текущее capacity на 75% ее размер увеличивается в двое. Расширение HashMap также происходит предельно просто - выделяется новая хэш таблица, в цикле проходимся по старой таблице и перекладываем ее элементы в новую таблицу. 

Вывод

  
В общем признаться честно - java меня разочаровала. Единственное достоинство реализации HashMap - это простота реализации. Все просто и понятно. Негде там ошибиться. 
По сравнению с этим реализация map[] - это вершина инженерной мысли. Как Porshe 911 по сравнению со старым фордом. В map[] экономится каждый бит. Многих оптимизаций без хорошего понимания ассеблера просто не понять. Количество аллоцируемых объектов,  ссылок в Golang map[] на порядок меньше чем в HashMap, а это значит что и  гораздо меньше работы для сборщика мусора. Также гораздо выше memory locality. В Golang даже floating point арифметику лишний раз стараются не использовать, заменяя сдвигами и другими целочисленными операциями.  В общем мне стало немного понятнее почему софт на Java так тормозит.