Пользователь
0,0
рейтинг
6 декабря 2007 в 15:14

Разработка → Прощай, if $DEBUG!

Perl*
Думаю, любой программист на Perl довольно регулярно добавляет в программу вспомогательный код, который не должен выполняться всегда. Это может быть отладочный код, сбор статистики о скорости работы разных частей кода, вывод в лог, и т.д. С этим кодом связано сразу несколько проблем:
  1. Он мешает читать основной код.Он замедляет выполнение программы.Из-за первых двух причин его зачастую удаляют, как только необходимость в нём пропадает… только, к сожалению, необходимость в нём регулярно возникает снова, и этот код, матерясь, снова пишут… чтобы через несколько часов снова удалить.Борьба с первой проблемой, как правило, обречена на неудачу. Ибо если код должен выполняться, то он должен быть написан. А если он написан, то он царапает глаза, разрывает основной код, раздувает код, отвлекает и раздражает. Решить эту проблему, как правило, удаётся только тогда, когда этот код должен быть написан в самом начале и/или конце функции — тогда можно автоматически сгенерировать функцию-обёртку, которая спрячет внутри себя этот код.

    А вот со второй проблемой бороться можно вполне успешно:
    warn "i=$i\n" if $DEBUG;
    В этом случае потери производительности когда $DEBUG==0 ограничиваются проверкой одного скаляра, т.е. фактически код получается такой:
    0;

    К сожалению, это решение приводит к тому, что «лишний» код становится ещё больше: в каждой такой строке добавляется if $DEBUG. С этим пытаются иногда бороться, перенося этот if внутрь вызываемой функции:
    sub log { return if !$DEBUG; warn "$_[0]\n"; }
    log("i=$i");

    К сожалению, производительность при этом падаёт значительно сильнее, т.к. к проверке скаляра добавляется ещё и вход/выход в функцию. А поскольку смысл существования $DEBUG в том, чтобы производительность не падала, то этот способ обычно не пользуется популярностью.

    Ещё один вариант — использование технологии source filters. С её помощью можно изменить код программы перед тем, как perl начнёт её компилировать и выполнять. Изменить как любой другой текст, обычно с помощью регулярных выражений. Например, код превратить в комментарии, либо наоборот — это позволит полностью избежать замедления программы (например, посмотрите модуль Smart::Comments). Первая проблема этого подхода в том, что синтаксис языка Perl очень сложный, а регулярные выражения использующиеся для модификации кода программы (обычно довольно простые) иногда ошибаются… и искать и исправлять такие баги довольно сложно. Вторая проблема заключается в том, что появляется ощущение потери контроля над своим кодом — вы уже не знаете точно, какой именно код выполняется, т.к. ваш код был как-то модифицирован. Парадокс, но вторая проблема полностью сводит на нет смысл существования таких модулей — ведь они изначально появились чтобы облегчить работу с кодом убрав из него «лишние» команды, а в результате работа с кодом наоборот, усложнилась.

    Я хочу предложить вашему вниманию другой способ решения этой проблемы:
    BEGIN {
      *log = $ENV{DEBUG} ? sub {warn "@_"} : sub () {0};
    }
    log+ "data", 123;

    Идея в том, что при отключенном отладочном режиме функция log определяется как константа, а функции-константы perl оптимизирует и просто подставляет в код их значения вместо того, чтобы их вызывать. (Код BEGIN { *log = sub () {0} } идентичен коду use constant log => 0;.) В результате реальный код, который выполнит perl будет:
    log( +"data", 123 ); # if $ENV{DEBUG}
    0 + "data", 123;     # if !$ENV{DEBUG}

    В результате от if $DEBUG мы избавились, и функция не вызывается, когда отладочный режим выключен. (А когда отладочный режим включен — лишний унарный плюс никак испортить первый аргумент функции не должен.)

    К сожалению, у этого изврата есть два побочных эффекта. Первый — use warnings ругается на попытки сложения не-чисел. Второй — этот способ всё же немного медленнее варианта с if $DEBUG, т.к. аргументы функции log вычисляются даже в том случае, когда отладочный режим отключен.

    Впрочем, несмотря на недостатки, этот способ имеет право на существование, и, возможно, будет кому нибудь полезен. К тому же, придумал я его всего пару часов назад, и возможно его ещё удастся развить и избавить от недостатков.
Alex Efros @powerman
карма
302,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (36)

  • 0
    Как вариант, вместо унарного плюса можно использовать амперсанд, при условии что первый аргумент функции - другая функция или константа:

    use constant INFO => 1, ERR => 2;
    log &INFO, "message";
    log &ERR, "message";
    • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        Угу. Только логики приложения за этим мусором уже не видно давно, а так всё в порядке...
  • 0
    А это не поможет?
    • 0
      Не поможет что? Вести логи с разной степенью детализации? Замерять скорость работы разных участков кода? Нет, не поможет...

      Более того, большинство проблем лично мне удобнее и значительно быстрее отлаживать вставляя в код warn-ы, нежели в отладчике. Да, я понимаю, звучит дико и пахнет седой древностью, но... это реально удобнее и быстрее во многих случаях.
  • 0
    А нельзя если дебаг равен труЪ, то что-нить заинклюдить? Не верю что от одного условия вся производительность упадёт.
    • 0
      Проблема в том, что код, который нужно "заинклюдить" должен быть равномерно распределён по всему коду приложения. Ну, представьте себе, к примеру, вывод в лог. Для этого по всему приложению нужно в ключевых местах распихать команды вывода в лог, указывая везде разные log level и данные, которые нужно вывести в лог конкретно в этом месте. И как вы предлагаете все эти команды "заинклюдить"? Не родился ещё такой мощный инклуд, чтобы взять команды из одного файла и равномерно распределить их по другому, в нужных местах. :)
      • 0
        Да, чёто ступил, сам же дебажу так как вы пишите.
        ИМХО: Хотя вызов функции лог тоже помойму жрёт немного, а её вот инклудить если дебаг==труЪ а если иначе то создавать функцию — пустышку. Обычно логаются же уже существующие переменные, а не мега красивый лог верстается, чтобы потом друзьям показать «зацените, как я красиво дебажу»… :)
  • 0
    На этапе компиляции Перл вычисляет все константные условия, так что вся статья, честно говоря, ни к чему - писать:
    warn 'something' if DEBUG;
    когда DEBUG объявлено как
    use DEBUG => 0;
    равносильно пустой строке, а когда
    use DEBUG => 1;
    просто warn 'something';

    На этапе выполнения 0 и 1 не вычисляются - условие просто отбрасывается!

    Это было в одном из рецептов отладки под mod_perl в mod_perl Cookbook.
    • 0
      Суть статьи была в том, чтобы избежать замусоривания кода текстом "if DEBUG".
      • 0
        Но ведь вы фактически предлагаете заменить одно выражение на другое. На самом деле вы просто оптимизировали вариант с вызовом функции. Я бы все таки отдал предпочтение фильтрам и сложность синтаксиса в таком случае не должна пугать, а иначе в сад .. т.е. может быть python/php (как вариант)? ;)
        • 0
          Развёрнутый ответ по поводу фильтров ниже.
  • 0
    1. классический вариант ничем не хуже:
    perl -MO=Deparse -e 'sub DEBUG() {0};warn "DEBUG" if DEBUG;warn "!DEBUG" if !DEBUG;'
    perl -MO=Deparse -e 'sub DEBUG() {1};warn "DEBUG" if DEBUG;warn "!DEBUG" if !DEBUG;'
    2. ваш вариант тоже неплох, только вот ошибочка:
    не будет 0+"data", а 0, (т.е. операция + не будет выполняться в рантайме) ну и варнинг в нагрузку.
    perl -MO=Deparse -e 'BEGIN{*lg=sub(){0}} lg+ "data", 123;'
    • 0
      1. У классического варианта свои недостатки. Во-первых в каждой строке болтается "if DEBUG". Во-вторых, когда нужно управлять, например, детализацией лога, if-ы становятся ещё и разнообразными, что тоже не идёт на пользу читабельности:
        log(INFO,"message") if LOG_INFO;
        log(ERR,"message") if LOG_ERR;

      2. Ещё точнее, вместо 0+"data" будет не 0, а "data". :) Я просто не видел смысла вдаваться в такие детали в статье, там важнее было описать какие эффекты будут в плане производительности, чем точно указать генерируемые perl-ом опкоды, IMHO.
  • 0
    Кроме извращенных решений perl предлагает более тривиальные способы управления кодом: рекомендую perldoc perlfilter, конкретнее Filter::cpp, хотя мне больше по душе встроенный препроцессор :) транслятор конфигурации за пару минут IMHO каждый может накидать.
    • 0
      Я про source filters в статье писал - в частности, указал на очень серьёзные, с моей точки зрения, недостатки.
      • 0
        Честно, не вижу тех проблем, которые Вы описываете. Боязнь потери контроля над кодом — это проблема не фильтров, а хрупкого кода. Ну а про ошибающиеся регулярные выражения даже говорить как-то странно. Но если Вы видите в этом проблему: можно для предотвращения ошибок выработать однозначные соглашения по меткам в коде.
        Почитал исходники фильтров (http://search.cpan.org/~pmqs/Filter-1.34/), не вижу никаких проблем, поправьте меня, может я что-то фундаментальное упускаю?
        • +1
          Угу. Упускаете. Как я уже говорил, синтаксис Perl довольно сложный, и написать корректный парзер очень тяжело. Возможно Вы слышали, что: "only perl can parse Perl". :)

          Как Вы думаете, что выведет вот этот код:

          use Filter::cpp;
          #define DEBUG 1
          #define STATUS (DEBUG ? "enabled\n" : "disabled\n")
          warn "DEBUG mode: ".STATUS;
          warn 'DEBUG mode: '.STATUS;
          warn q{DEBUG mode: }.STATUS;
          warn qq{DEBUG mode: }.STATUS;

          А вот что:

          DEBUG mode: enabled
          DEBUG mode: enabled
          1 mode: enabled
          1 mode: enabled

          И происходит эта лажа исключительно потому, что для perl и '' и "" и q{} и qq{} - это текстовые строки, а для Filter::cpp текстовыми строками являются только первые два варианта - иными словами Filter::cpp некорректно парзит Perl. И так обстоят дела с любыми source filters.
          (Да, теоретически возможно написать корректный парзер Perl на Perl для использования в source filters, но во-первых он будет очень не быстро работать, а во-вторых будет нереально тяжело поддерживать его совместимость с самим perl.)

          Так вот, упомянутая мной в статье проблема заключается как раз в том, что программист использующий source filter может быть абсолютно точно уверен, что этот source filter некорректно парзит Perl, но предположить ГДЕ и КАК он лажанётся - невозможно! Поэтому приходится при использовании source filter стараться использовать минимум возможностей perl, и всё-равно нет гарантии что он где-нить не лажанётся. А когда он лажанётся, найти этот баг будет крайне тяжело - ведь Вы не видите тот код, который реально выполняет perl после отрабатывания source filter.
          • 0
            Это еще больший изврат, чем предложенная функция в статье!
            Конечно если ходить по полю, с разложенными граблями можно убиться. Зачем использовать управляющие конструкции в исполняемом тексте? Абсолютной свободы не бывает.
            Задачу, которую Вы ставили перед собой — это повышение читаемости текста и отсутствие падения производительности . Первое решается на ура, любой редактор выделит эти конструкции цветом, они сразу станут блочними и будут выразительны. Второе имеет смысл только при частых запусках скрипта, что при использовании ускорителей или при работе демона не имеет смысла.
            В perl-е мне в очень нравится, что "все уже написано до нас", зачем изобретать велосипед, который ездит не по требованиям? Ваш вариант не решает этих проблем.
            • 0
              Я довольно часто пишу код для сложного асинхронного I/O, который должен работать очень быстро. Например код, который выкачивает 500 url/sec, выполняет довольно много операций при выкачке каждой url. Эти операции в отладочном режиме могут выводить в лог десятки сообщений. А это значит, что число вызовов функции log() измеряется десятками тысяч в секунду. Добавьте ко времени непосредственно самого вывода в лог, время на подготовку и оформление выводимых в лог данных. И получится, что скорость системы падает в разы, если не на порядок. Вот и приходится выкручиваться...
              • 0
                Я согласен, бывают очень требовательные приложения и не менее требовательные люди :)
                Фильтры мне самому не очень нравятся, я предлагаю использовать встроенный препроцессор. Т.е. при разработке Вы пишете #define и перед стартом транслируете в рабочий вариант с помощью Deparse.
                • 0
                  Да, вариант любопытный, я его рассматривал. Но меня смущало следующее:
                  1. получается несколько версий окончательного кода (смотря с какими директивами его генерить)
                  2. после каждого изменения кода требуется запуск препроцессора
                  3. в инете есть отличные статьи с описанием "чем плохо использовать препроцессор" (они писались для C, но принципы одинаковы для всех языков)
                  4. нет возможности "на ходу" включать/выключать логи или отладочный режим (например, послав процессу сигнал)
  • 0
    http://log4perl.sourceforge.net/ в помощь =)
    • 0
      Ненавижу bloatware!

      Log::Log4perl в 2.5 раза медленнее моей аналогичной реализации и в 16 раз медленнее if DEBUG.
      • 0
        Ваша реализация может в сислог писать и в собственные файлы?
  • 0
    откройте для себя Smart::Comments и не страдайте ерундой :)
    • +1
      Если бы Вы прочитали статью целиком, Вы обязательно заметили в ней ссылку на этот замечательный модуль.
  • 0
    Есть ещё такой вариант, скорость такая же, как при if (DEBUG) , а читабельность выше.

    use constant DEBUG => 0;
    DEBUG && warn "i=$i";
    • –1
      imho лучший способ. И подозреваю быстрее чем if
  • 0
    От проверки $DEBUG оверхед просто микроскопический. Ваша микрооптимизация только прибавит очередной пункт в code complexity
    • 0
      1. это не $DEBUG , а
      sub DEBUG {1} # или sub DEBUG {0}
      хотя, конечно же $DEBUG так же подойдёт.
      2. лично мне кажется, что строка начинающаяся со слова "DEBUG" самая понятная для обозначения отладки.
      3. Т.к. я всегда пишу IF одним и тем же способом, такая запись экономит лично мне 2 строки и часть символов
      4. Да, это слегка сложнее для понимания, т.к. эта запись не стандартизирована, хотя и является полным аналогом IF(){}
      P.S. IF я пиши так:
      if(DEBUG)
      {
          warn "i=$i";
      }
  • Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.