Первая публикация 28.12.2011
-Я кажусь вам академиком с большим задом,
Один, мол, я жрец поэзий непролазных.
А мне в действительности единственное надо –
Чтоб больше поэтов хороших и разных.
В. Маяковский «Послание пролетарским поэтам»
В статье речь пойдет о сравнении двух языков программирования на примере одной и той же задачи. Программисты и так прекрасно осознают, что одинаковая задача в разных системах программирования будет выглядеть по-разному и для очередного подтверждения этого никакой статьи и не требуется.
Но тут сыграли роль два обстоятельства:
Во-первых, последнее время часто сравниваются языки, которые не так уж и отличаются друг от друга. Т.е. парадигмы этих языков схожи. Интереснее и полезнее сравнить более «разные» языки.
Во-вторых, автору попались на глаза два фрагмента реальных программ, выполняющих одно и то же действие, но написанные людьми, живущими буквально в разных программных мирах. Жаль упускать такой случай и не провести сравнительный анализ, тем более что сама задача простая и понятная. Кроме того, получились очень естественные условия: программисты не знали друг о друге и не соревновались между собой. Каждый написал несколько строк самым простым для себя образом, не задумываясь, идеальное ли это решение, и приступил к программированию следующих фрагментов. Специально придуманные для сравнения тесты иногда выглядят надуманными как задачи, да и сил на их эффективное кодирование тратится очень много.
Все изложенное ниже, конечно, отражает личную точку зрения автора. Только для сокращения объема статьи я не вставляю в каждое предложение «с моей точки зрения» и «по-моему», хотя они там подразумеваются.
Итак, простая и реальная задача, на примере которой сравниваются две системы программирования — это расчет циклически избыточного кода с заданным полиномом. Для тех, кто не сталкивался с подобными вещами, поясню, что это контрольные данные, которые должны совпасть с заданными при проверке и подтвердить тем самым целостность исходных данных. На заре программирования использовалась просто сумма всех кодов (контрольная сумма), сейчас же это нечто вроде остатка от деления. Например, данные каждого сектора жесткого диска «защищены» циклически избыточным кодом. В отличие от контрольной суммы, при изменении даже одного бита в исходных данных циклически избыточный код меняется очень сильно, уменьшая вероятность случайного совпадения с правильным значением.
Реализация на языке C
// Заголовочный файл экспорта имен функций
#include «crc32.h»
// Полином для расчета контрольной суммы
#define CRC32_POLYNOM 0x04C11DB7
// Таблица для расчета контрольной суммы
unsigned long crc32tab[256];
// Процедура инициализации таблицы, используемой для расчета контрольной суммы
void
init_crc32_table()
{
……..int…..i, j;
……..unsigned long…….crc_accum;
……..for (i = 0; i < 256; i++) {
…………crc_accum = ((unsigned long)i << 24);
…………for (j = 0; j < 8; j++) {
……………if (crc_accum & 0x80000000)
……………….crc_accum = (crc_accum << 1) ^ CRC32_POLYNOM;
……………else
……………….crc_accum = (crc_accum << 1);
………….}
………….crc32tab[i] = crc_accum;
……..}
}
// Процедура расчета контрольной суммы массива байтов длиной len
// Проверка на валидность параметра len не проводится
unsigned long
calc_crc32(unsigned char *pdata, int len)
{
….unsigned long crc32 = 0xFFFFFFFF;
….unsigned char……k;
….while (len—) {
…….k = ((unsigned char)(crc32 >> 24) ^ *pdata++) & 0xFF;
…….crc32 = (crc32 << 8) ^ crc32tab[k];
…..}
….return (crc32);
}
Фрагмент взят именно так, как он написан в программе. Полагаю, большинству читателей понятно, как именно происходит расчет в этих нескольких строках.
А вот ниже приведен фрагмент, который, вероятно понятен старшему поколению программистов, а молодые вообще не сталкивались с таким языком и многое здесь не понятно. Но далее как раз и пойдет обсуждение и сравнение.
Реализация на языке PL/1
//─────── ПРОЦЕДУРА ПРОВЕРКИ CRC ─────────────
ПРОВЕРКА_CRC:PROC(P_ДАННЫЕ, ДЛИНА_ДАННЫХ) RETURNS(BIT);
DCL // CRC32 ВНЕШНЯЯ ПЕРЕМЕННАЯ BIT(32)
//—- ВХОДНЫЕ ДАННЫЕ — АДРЕС И ДЛИНА —-
P_ДАННЫЕ PTR,
ДАННЫЕ (1_000_000_000) BIT(8) BASED(P_ДАННЫЕ),
ДЛИНА_ДАННЫХ FIXED(31),
//—- ВСПОМОГАТЕЛЬНЫЕ ТАБЛИЦЫ И ПЕРЕМЕННЫЕ —-
CRC32TAB (0:255)……..BIT(32) STATIC,
CRC_ACCUM……………..BIT(32),
LAST_CRC………………….BIT(32),
//—- ИНДЕКСЫ КАК СЛОВА И КАК БАЙТЫ —-
(I,K)………….FIXED(31),
B_I……………BIT(8) DEF(I),
B_K…………..BIT(8) DEF(K);
//—- ОДНОКРАТНОЕ ЗАПОЛНЕНИЕ ТАБЛИЦЫ РАСЧЕТА CRC —-
1 DO I=0 TO 255;
…..CRC_ACCUM=B_I||’000000’B4;
…..DO K=0 TO 7;
……..LAST_CRC=CRC_ACCUM;
……..CRC_ACCUM=SUBSTR(CRC_ACCUM,2);
……..IF (LAST_CRC & ‘80000000’B4) = ‘80000000’B4 THEN
……….CRC_ACCUM=BOOL(CRC_ACCUM,CRC32_POLYM,XOR);
……END K;
……CRC32TAB(I)=CRC_ACCUM;
END I;
//—- СОБСТВЕННО ПРОВЕРКА CRC БЕЗ УЧЕТА СИНХРОБАЙТ —-
CRC32=’FFFFFFFF’B4;
DO I=1 TO ДЛИНА_ДАННЫХ;
…..B_K =BOOL(SUBSTR(CRC32,1, 8),ДАННЫЕ(I), XOR);
…..CRC32=BOOL(SUBSTR(CRC32,9,24),CRC32TAB(K),XOR);
END I;
//—- ВЫХОДИМ С РЕЗУЛЬТАТОМ СРАВНЕНИЯ И С CRC32 —-
RETURN(CRC32=’00000000’B4);
END ПРОВЕРКА_CRC;
Еще раз подчеркну, что это реальные фрагменты двух больших программ, выполняющих одно и тоже действие.
Сначала рассмотрим отличия, не являющиеся предметом данной статьи и вызванные просто разными стилями и привычками. Автор на PL/1 вставляет больше комментариев и пустых строк, а также пишет все большими буквами по его утверждению «для сохранения зрения». Если посчитать программные строки без комментариев, окажется, что в обоих фрагментах их почти одинаковое число. Также не является отличием возвращаемый результат, автор на Си просто возвращает контрольный код, а автор на PL/1 возвращает и результат сравнения и сам код (как внешнюю переменную). Очевидно, это опять зависит от стиля, а не от языка. Это же относится и к оператору сравнения. Автор на PL/1 выписывает все условие полностью:
IF (LAST_CRC & ‘80000000’B4) = ‘80000000’B4 THEN не ограничиваясь «результат равен/не равен нулю» и считает, что эта четкость условия способствует лучшему пониманию.
Бросается в глаза, что на языке Си описаны две процедуры: инициализация таблицы init_crc32_tabl и собственно расчет calc_crc32, а на языке PL/1 только одна, ПРОВЕРКА_CRC, причем утверждается, что внутри таблица заполнится только раз, а перед циклом ее заполнения стоит странный символ «1». Это отличие также оказывается «ненастоящим», поскольку речь идет о «самодельном» (не включенном в стандарт) расширении языка. Если его добавить в язык Си, оно будет выглядеть как 1{…}; Это действие означает одноразовое выполнение составного оператора. Такой оператор обычно и используется как сложная инициализация. В данной задаче его использование вполне естественно и вообще это довольно частый случай, позволяющий упростить интерфейс. А поскольку в PL/1 и цикл и составной оператор выглядят как DO … END; добавление единицы перед циклом превращает его в одноразовый. Реализовано это следующим образом: «префикс» 1 заменяется транслятором на системный вызов, который при своем выполнении меняет команду вызова на команду обхода составного оператора. Замечу, что это отличается, например, от вызова конструктора класса, который выполняется каждый раз при создании экземпляра. Кому-то может показаться неэффективным выполнение команды перехода при каждом выполнении процедуры. Однако, как говорят в Одессе, «я вас умоляю» — один безусловный переход не даст ощутимого снижения скорости работы.
Можно считать, что все предыдущее было введением, а вот сейчас и начинается обсуждение разных парадигм.
Первое и, пожалуй, главное отличие PL/1 и Си в данной задаче в том, что в PL/1 изначально предусмотрена работа с битовыми строками как с объектами встроенного типа «BIT», а в Си разрешены логические операции с числами. Парадигма PL/1: есть битовые строки – цепочки нулей и единиц, к которым применимы все действия булевой алгебры. Эти строки можно резать, склеивать, заменять/доставать в них фрагменты. В программе на PL/1 собственно это и делается, поскольку сама задача как раз и состоит в «перемешивании» цепочек нулей и единиц. Обратите внимание, что при такой парадигме не требуется и понятие «сдвиг». А ведь даже в стандарте Си сдвиг вправо не определен: может быть, а может и не быть расширение знаковым разрядом. При работе со строками бит не указанные разряды не меняются и обнуление их не требуется, сравните два расчета индексов:
k = ((unsigned char)(crc32 >> 24) ^ *pdata++) & 0xFF;
B_K = BOOL(SUBSTR(CRC32,1,8),ДАННЫЕ(I),XOR);
В языке Си операция «исключающее ИЛИ» входит в состав набора операций, а в языке PL/1 – нет. Вместо этого имеется встроенная функция BOOL, реализующая все 16 булевых операций (большая часть из которых, правда, довольна бессмысленна). Удобнее все же было бы и ее добавить в PL/1 к уже имеющимся И-ИЛИ-НЕ, так как на практике она довольно часто нужна (как в данной задаче).
Второе различие связано с преобразованием типов. Парадигма PL/1: если одни и те же данные должны использоваться как объекты разного типа, то можно описать эти данные (один и тот же участок памяти) как объекты разного типа. Это реализуется оператором описания DEFINED (или DEF как в фрагменте). В данной задаче как раз тот самый случай: индекс таблицы становится частью полинома. Никаких преобразований типов в PL/1-программе нет вообще, точнее они неявно (или наоборот, явно, это как считать) присутствуют в описании. В программе на Си потребовалось указать приведение к типам unsigned char и unsigned long. Вероятно, ни к каким дополнительным командам это и не приводит, однако в PL/1-программе преобразования гарантированно исключены самой парадигмой «наложения» объектов в памяти.
Вот, собственно, и все по части парадигм в этих маленьких примерах. Остальные отличия больше обусловлены привычными стилями и традициями, чем различиями языков. Кому-то понятнее указывать индекс массива: ДАННЫЕ(I), кому-то ставить два знака плюса перед указателем: *data++. В принципе, в программах это место можно было написать наоборот: на Си использовать индекс, а в PL/1 «наложить» число на указатель и прибавлять к нему единицу.
Ну, и что дает такое сравнение? Вроде получается «ничья»: для одинаковой задачи получились программы одинакового размера и, вероятно, примерно одинаковой эффективности при разных подходах.
Однако есть один штрих, посмотрите еще раз на основные операторы вычисления на Си и на PL/1:
k = ((unsigned char)(crc32 >> 24) ^ *pdata++) & 0xFF;
crc32 = (crc32 << 8) ^ crc32tab[k];
B_K =BOOL(SUBSTR(CRC32,1, 8),ДАННЫЕ(I), XOR);
CRC32=BOOL(SUBSTR(CRC32,9,24),CRC32TAB(K),XOR);
Во втором случае больше проглядывает одинаковость действий, замаскированная в первом случае использованием операции сдвига, хотя именно сдвиг по смыслу самой задачи и не требуется. Эта одинаковость является косвенным признаком, того, что данную задачу удается все-таки яснее выразить, используя парадигмы PL/1, а не Си. И если предложить человеку, незнакомому с обоими языками, разобраться в алгоритме вычисления циклически избыточного кода, то понятие битовых строк и их фрагментов могут оказаться понятнее.
Таким образом, вполне очевидный вывод, что желательно иметь больше парадигм «хороших и разных» находит еще одно подтверждение. Хотя рассмотренная задача довольно специфическая, она реально встретилась на практике. И наличие в языке таких объектов как битовые строки, а также возможности манипулировать с одним и тем же участком памяти как с объектами разного типа выглядит более естественным при реализации.