2 февраля 2016 г.

Массовые расстрелы и духовная практика смирения


— Микола, ты слыхал как профессиональные программисты тм наш newValue кличут?
— Не, а как?
— "о"!
— Да ну! Вот же ж нелюди, поубивал бы гадов!

Разбирался сегодня в одном из наших сервисов, который использует довольно старую версию trove (2.1.0)

Цитата из THashMap.Entry.setValue():
public V setValue(V o) {
    if (_values[index] != val) {
        throw new ConcurrentModificationException();
    }
    _values[index] = o;
    o = val; // need to return previous value
    val = o; // update this entry's value, in case setValue is called again
 
    return o;
}
Возьмите себе минуту времени на то, чтобы найти в нем ошибку.

Нашли?

Теперь так (o -> newValue):
public V setValue(V newValue) {
    if (_values[index] != val) {
        throw new ConcurrentModificationException();
    }
    _values[index] = newValue;
    newValue = val; // need to return previous value
    val = newValue; // update this entry's value, in case setValue is called again
 
    return newValue;
}
Стало очевиднее?

Возможно, что нет.

Тогда еще одна попытка (+final):
public V setValue(final V newValue) {
    if (_values[index] != val) {
        throw new ConcurrentModificationException();
    }
    _values[index] = newValue;
    newValue = val; // need to return previous value
    val = newValue; // update this entry's value, in case setValue is called again
 
    return newValue;
}
... упс, так написать нельзя – будет ошибка компиляции при присвоении значения
final переменной. Придется завести еще одну переменную (внимательный читатель
заметил, что заодно мы уже вынуждены исправить одну из ошибок – в .val
записывалось ее же старое значение, вместо нового):

public V setValue(final V newValue) {
    if (_values[index] != val) {
        throw new ConcurrentModificationException();
    }
    _values[index] = newValue;
    final V oldValue = val; // need to return previous value
    val = newValue; // update this entry's value, in case setValue is called again
 
    return newValue;
}
...Странно: мы хотим вернуть из метода старое значение, а возвращаем – новое.
Что-то здесь не так. Исправим:
public V setValue(final V newValue) {
    if (_values[index] != val) {
        throw new ConcurrentModificationException();
    }
    _values[index] = newValue;
    final V oldValue = val; // need to return previous value
    val = newValue; // update this entry's value, in case setValue is called again
 
    return oldValue;
}
Итого: 2 ошибки (!) в коде из 7 строчек. Совсем не очевидные в исходном варианте (я лично потратил несколько минут).

А чтобы эти ошибки стали очевидны (и, скорее всего, даже и не попали бы в репозиторий) достаточно было бы:
  1. объявить все локальные переменные и аргументы методов final
  2. назвать переменные своими именами
  3. не переиспользовать локальные переменные, а заводить новые (смотри так же пункт 2)
тогда на одну из этих ошибок укажет компилятор, а вторая становится достаточно очевидной глазу (Куда уж очевиднее: " – Что мы хотим сделать? – Вернуть старое значение. – А что написано? – return newValue")

Конечно, после драки завсегда легко кулаками помахать. Мне-то на исходную ошибку вообще указал findbugs (справедливости ради, я и не собирался ревьювить код trove). Однако в моей практике я регулярно сталкиваюсь с тем, что достаточно выполнить эти 3 пункта, и запутанные методы становятся понятными, а глупые ошибки начинают бросаться в глаза. Почему люди так не делают? Это третий величайший вопрос на свете после "откуда пыль берется" и "куда деньги деваются".


Когда я сталкиваюсь с подобными сценариями, у меня создается твердое ощущение, что многие программисты всерьез считают свой мозг самым крутым компьютером на свете. Типа – раз я управляю этим примитивным кремниевым i7, значит я умнее его. И поэтому я даже не буду предполагать, что мой мозг может запутаться в 7 строчках кода с 3 переменными – да ну, это невозможно, я же профессионал, я в такой ерунде не ошибаюсь. И зачем я буду помогать себе, выбирая осмысленные имена для переменных? Это для слабаков, настоящие профессионалы не нуждаются в таких костылях.

...Человеческий мозг может быть и превосходит по мощности – формально – любой существующий суперкомпьютер. Вот только у любого вычислителя есть сильные и слабые стороны, и слабенький DSP умножает матрицы в сотню раз быстрее, чем человеческий мозг. Математика, логика, синтаксический разбор, анализ потоков данных и синтаксических деревьев, программирование (в том виде, в котором мы его сейчас знаем) – не относятся к сильным сторонам человеческого мозга. Даже близко не относятся. То, что мы можем (лет за 5-10-20 ежедневного опыта) научить свой мозг как-то все это делать – вовсе не означает, что мозг делает это хорошо. Мозг не делает это хорошо, и никогда не будет, просто (пока) никто не делает это лучше, поэтому мы вынуждены решать эти задачи используя такой неподходящий биологический компьютер. Я, может, неожиданную мысль скажу, но никто в мире не программирует хорошо. Хорошо работает компилятор: javac на моем компьютере распарсит и скомпилирует несколько тысяч файлов за пару десятков секунд без единой ошибки – вот это хорошая работа. Кто может похвастаться, что превратил несколько тысяч страниц бизнес-спецификации в код без единой ошибки хотя бы за неделю? Хоть раз в жизни? Никто таким не может похвастаться, нет таких людей. Никто из нас не программирует хорошо, по 5-бальной шкале мы все в пределах тысячных долей от нуля. Мы занимаемся этим только и исключительно потому, что компилятора не формализованных бизнес-спецификаций пока не придумали.

К чему это я? К тому, что, раз уж эта работа для мозга сложная, то необходимо использовать любые способы помочь мозгу делать эту сложную работу, к которой он совершенно не подготовлен эволюционно. Любые способы как-то подключить сильные стороны мозга к решению столь непрофильных задач. Давая же бессмысленные имена переменным, переиспользуя их где ни попадя – я усложняю мозгу и без того сложную задачу.


...Только массовые расстрелы регулярная практика смирения спасет IT-отрасль. Ежеутренняя получасовая медитация, прямо перед стэндапом: "Я обычный человек, мой мозг слаб, грешен, и подвержен ошибкам. Мой мозг не способен надежно оперировать более чем 3-4 единицами информации одновременно. Мой мозг нуждается в подсказках. Никто не способен писать код без ошибок, и я тоже не способен. Даже если очень стараюсь – в моем коде есть ошибки. Моя задача – сделать мои ошибки очевидными любому, кто читает мой код..."

P.S. На всякий случай: в какой-то версии trove старше 2.1.0 эта ошибка была исправлена. Я не смог отследить, в какой именно: в старом CVS репозитории ошибка присутствует везде, в "новом" SVN репозитории ее нет.

26 комментариев:

  1. > trove

    Зачем, когда есть fastutil/GS/koloboke/hppc/hppc-rt?

    ОтветитьУдалить
    Ответы
    1. trove 2.1.0 это 2011, если не раньше. В то время половины вышеупомянутого еще не было, а мы с тобой живого Unsafe в руках не держали.

      Кроме того разница между j.u.HashMap и тем же THashMap -- приличная, как по памяти так и по скорости. А вот разница между trove/fastutil/GS/koloboke/hppc/hppc-rt уже менее заметна. То есть если нужно просто что-то лучше j.u.HM, то любой вариант подойдет, и выбор происходит просто по уровню знакомства. Если же нужно что-то прямо максимально быстрое, то имеет смысл выбирать тщательнее -- но здесь не тот случай.

      Удалить
  2. это чтобы не обфускачить - сразу двух зайцев убить.

    у нас народ прочно сидит на трове, хотя вот пинаю их на fastutil или колобок перевести, но в начале code style типа тех же checkArgument и @NotNull по-умолчанию - от трова еще дурацкая привычка возвращать 0... хотя это может быть и валидное значение

    словом - стираем пыль

    ОтветитьУдалить
    Ответы
    1. Да, мне в trove тоже не хватает чего-то типа getOrDefault(key, defaultIfNotFound)

      Удалить
  3. Я тут как то измерял troove vs kolobok
    troove как то заметно производительнее оказался
    Не знаю может я что то неправильно готовил

    ОтветитьУдалить
  4. Руслан проблема человеческого мозга не в том что он не умеет считать или писать код - а в том что ему постоянно мешают.

    Мозг успешно в состоянии используя воображение конструировать
    великолепные вычислительные модели.

    Если эти модели правильные - то код будет писать просто и правильно.






    ОтветитьУдалить
    Ответы
    1. >Мозг успешно в состоянии используя воображение конструировать
      великолепные вычислительные модели.

      Я тут с тобой не соглашусь. Позиция "я могу великолепно кодить, если мне не мешают" приводит, как по мне, к тому, что да, когда мне не мешают, я создаю великолепные модели. Которые никто потом не поймет -- без такой же многодневной изолированной медитации.

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

      >Если эти модели правильные - то код будет писать просто и правильно.

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

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

      Удалить
    3. Лично я еще не встречал кода - который бы всем нравился

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

      Это я к чем - а к тому что люди не способны асбтрагироваться от личных
      предпочтений при чтении кода и воспринять код таким как он
      написан.

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

      В итоге имеем километровые PR-ы в результате обсуждения в которых
      все равно заливается говнокод)

      По моей практике код ревью чем то напоминает молитву за упокой)
      Все все понимают но за каким то хреном молятся )

      Если же у нас случай когда человек тупо говнокодит с полным непониманием
      что он пишет - тот тут нужен не код ревью - а перевод человека на
      другой проект.

      Удалить
    4. А по поводу медитаций - в одиночку получается решать задачи намного эффективнее чем в команде. Вопрос того поймет ли кто потом и что вторичен - но все что пишется в командах - это конечно всем очень нравится (все это понимают вместе херачили) но как правило лишено прекрасной мысли и содержания.

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

      Ревью -- это иной взгляд на твой код. Я сам свои PR когда просматриваю в веб-интерфейсе stash -- часто нахожу и баги, и опечатки, и просто непонятные места. Потому что иначе все смотрится, чем в IDEA, и мозг начинает замечать что раньше не замечал.

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

      Удалить
    6. В целом соглашусь про психологию ревью. Так оно и есть. )))
      Я помню как мне пытались задать кузькину мать и в результате так загонялись что начинали нести полную ахинею ))))

      Удалить
    7. Кстати отличительная особенно в работе в нашей команде и в западной в том что иностранцы в работе весьма часто говорят

      -- Парни спасибо за хорошее ревью
      -- Молодец хорошая работа
      -- Молодец хороший код

      И вообще как то стараются больше позитив и уважение поддерживать.

      Удалить
    8. Это да, это известная особенность нашей культуры. Сам в себе регулярно замечаю, что вот найти в коде коллеги ошибку или недостаток -- это завсегда с удовольствием, а похвалить коллегу за то, что у него нет ошибок, или здесь хорошее решение -- вот как-то даже в голову не приходит. Нужно чтобы это было не просто хорошее решение, а прямо выдающееся что-то, чтобы "ВАХ!!!111" вызывало -- тогда да, тогда руки потянутся на этот счет комментарий написать. А вот за просто хорошо написанный код хвалить -- руки не тянутся.

      Удалить
  5. вау! таки да - мы таки затриггерили trove'ом баг в компиляторе в jvm - правда проблема уже известна - https://bugs.openjdk.java.net/browse/JDK-8081379
    но всё же!

    ОтветитьУдалить
    Ответы
    1. Вам бы, Владимир, только бы JVM поломать!

      Удалить
    2. оно само...

      +Roman Leventov: могёт ли колобок так нагнуть jvm ? ;)

      Удалить
    3. Колобок, по-моему, так Unsafe использует в хвост и в гриву, что нагнуть jvm ему вообще не проблема. Проблема -- не нагнуть )

      Удалить
    4. Кто нибудь измерял troove на огромном количестве данных ?
      100 гигабайт например ?

      Что то мне подсказывает что 100 гигабайт нагнут troove :)
      Вернее не troove а Garbage Collector )

      Удалить
    5. А как ты запихнешь 100Гб в коллекцию на базе линейного массива, который в яве ограничен 2^31 элементами? Максимум лонгов на 16Гб в один массив впихнуть можно.

      Удалить
    6. А String - коллекций там нету ?
      Лично проверял - есть )

      Удалить
  6. Ответы
    1. мощщщщно! 3 ошибки в 7 строках + одна в фиксе)

      Удалить
    2. Исправил, спасибо за внимательность :)

      Удалить
  7. Отличный статьюн!

    Я примерно такими же аргументами защищаю необходимость писать юнит-тесты.

    ОтветитьУдалить
  8. На самом деле ошибка там только одна - val не меняется (иначе ошибку обнаружили бы раньше) поскольку сначала о становится равным ему, после чего val фактически присваивается сам себе:

    o = val; // need to return previous value
    val = o; // update this entry's value, in case setValue is called again

    Так что возвращается старое значение, ошибка внесена при исправлении (при добавлении переменной oldValue её следовало бы использовать и ниже).

    ОтветитьУдалить