Оптимизация строковых штатных Си-функций
С разительным отличием скорости обработки двойных слов и байтов мы уже столкнулись (см. "Обработка памяти байтами, двойными и четвертными словами"). Теперь самое время применить наши знания для оптимизации строковых функций.
Типичная Си-строка (см. рис. 41) представляет собой цепочку однобайтовых символов, завершаемую специальным символом конца строки – нулевым байтом (не путать с символом "0"!), поэтому Си-строки так же называют ASCIIZ-стоками ('Z' – сокращение от "Zero", – нуль на конце). Это крайне неоптимальная структура данных, особенно для современных 32?разрядных процессоров!
Основной недостаток Си-строк заключается в невозможности быстро определить их длину, – для этого нам приходится сканировать всю строку целиком в поисках завершающего ее нуля. Причем, поиск завершающего символа должен осуществляется побайтовым сравнением каждого символа строки с нулем. А ведь производительность побайтового чтения памяти крайне низка! Конечно, можно попробовать сделать "ход котом": загружать ячейки памяти двойными словами, а затем "просвечивать" их сквозь четыре битовых маски, только вряд ли это добавит производительности, – скорее лишь сильнее ухудшит ее.
По тем же самым причинам невозможно реализовать эффективное копирование и объединение Си-строк. Действительно, как прикажете копировать строку не зная какой она длины?
В довершении ко всему, Си-строки не могут содержать символа нуля (т.к. он будет ошибочно воспринят как завершитель строки) и потому плохо подходят для обработки двоичных данных.
Всех этих недостатков лишены Pascal-строки, явно хранящие свою длину в специальном поле, расположенном в самом начале строки. Для вычисления длины Pascal-строки достаточно всего одного обращения к памяти (грубо говоря, длина Pascal-строк может быть вычислена мгновенно). /* Кстати, при работе с любыми структурами данных, в частности, со списками, настоятельно рекомендуется сохранять в специальном после ссылку на последний элемент, чтобы при добавлении новых элементов в конец списка не приходилось каждый раз трассировать его целиком */
Как это обстоятельство может быть использовано для оптимизации копирования и объединения Pascal-строк? А вот смотрите:
char *с_strcpy(char *dst, char *src) char *pascal_strcpy(char *dst, char *src)
{ {
char * cp = dst; int a;
while( *cp++ = *src++ ); for(a=0; a < ((*src+1) & ~3); a += 4)
// копируем строку по байтам *(int *)(dst+a)=*(int *)(src+a);
// одновременно с этим проверяя // копируем строку двойными словами
// каждый символ на равенство нулю // проверять каждый символ на равенство
// нулю в данном случае нет необходимости
// т.к. длина строки наперед известна
for(a=((*src+1) & ~3); a<(*src+1); a ++)
*(char *)(dst+a)=*(char *)(src+a);
// копируем остаток хвоста строки
// (если он есть) по байтам.
// это не снижает производительности,
// т.к. максимально возможная длина
// хвоста составляет всего три байта
return( dst ); return( dst );
} }
Листинг 39 Пример реализации функций копирования Си (слева) и Pascal строк (справа)
char *с_strcat (char *dst, char *src) char *pascal_strcat (char *dst, char *src)
{ {
char *cp = dst; int len;
while( *cp ) ++cp; len=*dst;
// читаем всю строку-источник // за одно обращение к памяти
// байт за байтом в поисках // определяем длину строки-приемника
// ее конца
*dst+=*src;
// корректируем длину строки-приемника
while( *cp++ = *src++ ); pascal_strcpy(dst+len,src);
// байт за байтом дописываем // копируем строку двойными словами
// источник к концу приемника,
// до тех пор пока не встретим нуль
return( dst ); return( dst );
} }
Листинг 40 Пример реализации функций объединения Си (слева) и Pascal строк (справа)
Итак, в отличие от Си-строк, Pascal-строки допускают эффективную блочную обработку и при желании могут копироваться хоть восьмерными словами. Другое немаловажное обстоятельство: при объединении Pascal-строк нам незачем просматривать всю строку-приемник целиком в поисках ее конца, поскольку конец строки определяется алгебраическим сложением указателя на начала строки с первым байтом строки, содержащим ее длину.
Интенсивная работа с Си-строками способна серьезно подорвать производительность программы и потому лучше совсем отказаться от их использования. Проблема в том, что мы не можем "самовольно" перейти на Pascal-строки, не изменив все сопутствующие им библиотеки языка Си и API-функций операционной системы. Ведь функции наподобие fopen или LoadLibrary рассчитаны исключительно на ASCIIZ-строки и попытка "скормить" им Pascal?строку ни к чему хорошему не приведет, – функция, не обнаружив в положенном месте символа-завершителя строки, залезет совершенно в постороннею память!
Выход состоит в создании "гибридных" Pascal + ASCIIZ-строк, явно хранящих длину строки в специально на то отведенном поле, но вместе с тем, имеющих завершающий ноль на конце строки. Именно так и поступили разработчики класса CString библиотеки MFC, распространяемой вместе с компилятором Microsoft Visual C++.
Рисунок 51 0х41 Устройство Си, Pascal, Delphi и MFC-строк.
Си- строки могут иметь неограниченную длину, но не могут содержать в себе символа нуля, т.к. он трактуется как завершитель строки. Pascal-строки хранят длину строки в специальном однобайтовом поле, что значительно увеличивает эффективность строковых функций, позволяет хранить в строках любые символы, но ограничивает их размер 256 байтами. Delphi-строки представляют собой разновидность Pascal-строк и отличаются от них лишь увеличенной разрядностью поля длины, теперь строки могут достигать 64Кб длины. MFC-строки – это гибрид Си и Pascal строк с 32-битным полем длины, благодаря чему макс. длина строки теперь равна 4Гб.
Несложный тест (см. [Memory/MFC.c]) поможет нам сравнить эффективность обработки Си- и MFC-строк на операциях сравнения, копирования и вычисления длины. На последней операции, собственно, и наблюдается наибольших разрыв в производительности, достигающих в зависимости от длины "подопытной" строки от одного до нескольких порядков.
Объединение двух MFC-строк (при условии, что обе они одинаковой длины) осуществляется практически вдвое быстрее, чем аналогичных им Си-строк, что совсем неудивительно, т.к. в первом случае мы обращаемся к вдвое меньшему количеству ячеек памяти. Разумеется, если к концу очень длиной строки дописывается всего несколько символов, то выигрыш от использования MFC-строк окажется много большим и приблизительно составит: крат.
А вот сравнение Си- и MFC- строк происходит одинаково эффективно, точнее одинаково неэффективно, поскольку разработчики библиотеки MFC- предпочли побайтовое сравнение сравнению двойными словами, что не самым лучшим образом сказалось на производительности. Забавно, но штатная функция strcmp из комплекта поставки Microsoft Visual C++ (не intrinsic!), – похоже единственная функция сравнения строк, обрабатывающая их не байтами, а двойными словами, что в среднем происходит вдвое быстрее. В общем, наиболее предпочтительное сравнение MFC-строк выглядит так:
#include <String.h>
#pragma function(strcmp) // вырубаем intrinsic'ы
if (strcmp(s0.GetBuffer(0),s1.GetBuffer(0)))
// строки не равны
else
// строки равны
Листинг 41 Пример эффективного сравнения MFC-строк
Рисунок 52 graph 24 Сравнение эффективности MFC и Си функций, работающий со строками. Как видно, MFC строки более производительны