Макросы: Условная компиляция

Для чего нужна условная компиляция и что это вообще такое?

Условная компиляция — это механизм формирования/генерации различного кода в зависимости от внешних условий, заданных на период начала компиляции.

Для чего это нужно? Ну, например, вы сможете генерировать из одного и того же исходного кода разные программы в зависимости от дня недели или наблюдаемой частоты вспышек на Солнце. Шучу конечно, но суть в общем-то отражает верно: это нужно для того, чтобы вы могли собрать программу, адаптированную к тем или иным условиям её (многократного) запуска. Чаще всего в данном контексте вспоминают рихтовку программного кода под целевую платформу: подключаются одни библиотеки, не подключаются другие, в зависимости от разрядности целевой платформы используются одни типы данных и не используются другие (например, на моём древнем нетбуке было бы актуально использовать Int32 вместо Int64). Тем не менее есть и другие, не менее значимые области применения: например, вы можете собрать версию программы с урезанным функционалом, но потребляющую существенно меньше памяти и/или более быструю, вы можете собрать код, из которого будут выброшены за полной бесполезностью условные конструкции, обречённые проверять незыблемые вещи. Таким образом, выудив пару-тройку назойливых if'ов из цикла с большим количеством итераций, вы малой кровью добьётесь весьма ощутимого прироста производительности: ведь проверка условий и условные переходы легко сбивают с толку конвейерный механизм работы современного микропроцессора (префетчинг/предвыборку команд в конвейер) и как минимум загружают его блок предсказания ветвлений ненужной работой.

Ну и конечно условная компиляция помогает быть сухим и чистым, т.е. «не повторять себя» (см. принцип d.r.y) и писать один макрос, ветвящийся в зависимости от значений того или иного параметра — там, где в противном случае пришлось бы писать несколько однотипных макросов или, что ещё «оригинальнее», генерировать макросами другие макросы.

В языке Crystal вы можете даже, пользуясь проверкой flag?(:release), вырезать из финального кода всё, что в противном случае прямо в рантайме проверяло бы, нужно ли выводить какие-нибудь низкоуровневые отладочные сообщения или же делать это и вовсе не следует. «Классический» подход к логированию заключается в том, чтобы устанавливать для приложения уровень отладки в конфигурационном файле или же задавать его параметрами командной строки. Но что если в этом нет никакой объективной необходимости? Что если ваш заказчик не хочет видеть отладку, а вы не хотите тестировать и отлаживать приложение прямо на «живом» заказчике? И если никогда не меняющееся условие «текущий_уровень_отладки >= debug» можно проверить один раз — при компиляции, — то почему бы так и не поступить? И машинного кода в релизе будет поментше, и работать он будет быстрее… Профит? Ну вот то-то же :)

Как же выглядит разрекламированная мной возможность языка Crystal впечатывать один код и не впечатывать другой, называемая также условной компиляцией?

Памятуя об упомянутой выше адаптации под архитектуру, вот вам классический solution для классического же use case, а именно генерация типа-псевдонима для того типа целого, который наиболее актуален для целевой архитектуры:


{% begin %}
    {% long_bl=64; gen_bl=32 %}
    {% if flag?(:x86_64) %}
        {% long_bl=long_bl*2; gen_bl=gen_bl*2 %}
    {% else %}
    # do nothing here
    {% end %} 
    alias Integer = Int{{gen_bl}} | UInt{{gen_bl}}
    alias LongInt = Int{{long_bl}} | UInt{{long_bl}}
    x: Integer=11_u{{gen_bl}}
    y: LongInt=12_i{{long_bl}}
    {% for var in %w(x y) %}
        puts "run-time class of var {{var.id}} is #{{{var.id}}.class}"
    {% end %}
{% end %}


Помимо некоторых интересных особенностей весьма умного движка макросов в языке Crystal, в данном примере на всякий случай показаны и некоторые его (языка) весьма досадные «родовые пятна»: например, для для того, чтобы поместить целое число в переменную целого же типа (не совпадающего с «умолчательным», зависящим от того, под какую архитектуру собран сам Crystal) — компилятору почему-то необходима вот такая откровенно уродливая конструкция, использующая некие самопальные суффиксы, изобретённые разработчиками из Manas, главного спонсора создания Crystal, не иначе как в горячем латинском ночном бреду:

x : Int128 = 11_i128

Видите вот это отвратительное "_i128"? Так вот, скоро вы его не увидите, ну или, вполне возможно даже, что на момент прочтения вами настоящей заметки — уже и не увидите, если только не поставите себе устаревшую версию Crystal. Дело в том, что Crystal 0.24.2, которым я пользуюсь сейчас — это один из последних выпусков, в которых упомянутая выше суффиксная кривизна всё ещё присутствует. Разработчики в курсе проблемы — и находятся на пути к скорейшему её решению.

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

Не переключайтесь и до новых встреч в прямом эфире!

0 комментариев