Применение подстроки в реализации быстродействующей строковой системы на c++

УДК 519.68 М. Ю. Орехов

Вестник СПбГУ. Сер. 10. 2015. Вып. 2

ПРИМЕНЕНИЕ ПОДСТРОКИ В РЕАЛИЗАЦИИ БЫСТРОДЕЙСТВУЮЩЕЙ СТРОКОВОЙ СИСТЕМЫ НА C++

Санкт-Петербургский государственный университет, Российская Федерация, 199034, Санкт-Петербург, Университетская наб., 7/9

Малое время выполнения операций сравнения и копирования строк является необходимым условием разработки приложений, быстродействие которых определяется скоростью синтаксического анализа и генерации текстовых файлов значительного объема. В качестве примера подобного приложения в статье рассмотрена графическая система визуализации, предназначенная для создания, редактирования, обработки и воспроизведения в реальном времени векторных графических схем открытого текстового формата, перечислены ее функциональные возможности, реализуемые при наличии надежной быстродействующей строковой системы. Предложен подход к проектированию ASCII строковой системы, основанный на широком использовании подстроки как универсального аргумента ее функций, который делает возможным реализацию операций сравнения и копирования строк, быстрых настолько, насколько позволяют низкоуровневые средства стандартной библиотеки C+—+, в том числе за счет значительного снижения числа обращений к динамической памяти. Определены классы «подстрока» и «строка». Описаны их ключевые свойства и методы. Приведено обоснование выбора низкоуровневой функции сравнения подстрок. Отмечены особенности настройки применения встроенных функций компилятора при разработке строковой системы. Представлен результат оценки быстродействия спроектированного строкового типа в соотнесении с аналогами, предлагаемыми разработчиками библиотек STL и Qt. Библиогр. 3 назв. Ил. 3.

Ключевые слова: строковый тип, обработка строк, сравнение строк, синтаксический анализ, парсер.

M. Yu. Orekhov

SUBSTRING EMPLOYMENT IN C++ QUICK-OPERATING STRING SYSTEM IMPLEMENTATION

St. Petersburg State University, 7/9, Universitetskaya embankment, St. Petersburg, 199034, Russian Federation

Applications where the operating rate is determined by speed of parsing and generating large text files require rapid string comparing and copying. This paper states the idea of substring, whose employment in ASCII string system implementation allows supporting the swiftest string comparing and copying operations, which can be in principle implemented using low-level means provided by C+—+ standard library. The present article defines substring class, describes its distinctive features and substantiates the choice of low-level function used for comparing substrings. The paper also marks peculiarities of using compiler intrinsics in string system design. Bibliogr. 3. Il. 3.

Keywords: string type, string processing, string comparing, syntactic analysis, parser.

Введение. Для обозначения контекста применения быстродействующей строковой системы на C+—+ рассмотрим графическую систему визуализации в составе информационно-вычислительного программного комплекса. Переход от процедурно-ориентированного подхода к объектно-ориентированному при разработке подобных графических систем находит отражение в отказе от использования закрытого бинар-

Орехов Михаил Юрьевич — аспирант; e-mail: genazvale2005@yandex.ru

Orekhov Mikhail Yurievich — post-graduate student; e-mail: genazvale2005@yandex.ru

ного формата для создания динамически отображаемых графических схем и применении для этих целей открытого текстового формата. Таким образом, быстродействие графической системы становится критически зависимым от скорости работы строковой системы при обработке и динамическом отображении схем.

Перечислим некоторые функциональные возможности графической системы, реализация которых требует быстрой обработки строк:

• динамическое отображение графических объектов схемы с частотой обновления видеокадра (минимум 4-5 FPS), позволяющей отнести систему визуализации к классу систем реального времени. Скорость изменения визуальных эффектов отображения векторного примитива определяется быстротой доступа к значениям его графических атрибутов. Механизм доступа к величине атрибута по его имени в структуре данных (контейнере) графического объекта дает возможность расширить функциональность и упростить разработку подсистемы динамического обмена. При этом скорость работы этого механизма напрямую зависит от надежности и быстродействия используемых контейнерной и строковой систем;

• чтение и запись схем. Применение универсальных текстовых форматов графических схем системой визуализации накладывает требование быстрого исполнения операций разбора и генерации текстовых файлов значительного объема с образованием сверхбольших таблиц идентификаторов, свойственных специализированным трансляторам и интерпретаторам. Разбор текста предполагает выделение его фрагментов - подстрок - и передачу их в качестве аргументов функциям строковой системы;

• редактирование схемы, в том числе операции отмены и повтора модификаций, копирования и вставки выборки объектов. Перечисленные операции подразумевают манипулирование и обмен фрагментами схем. Если же схема имеет открытый текстовый формат, ее фрагменты целесообразно представлять в виде строк, для получения и обращения с которыми используются процедуры чтения и записи (см. выше), что, в свою очередь, предполагает работу парсеров и генераторов при выполнении операций редактирования;

• поддержка библиотек графических объектов и их свойств. Библиотека может быть реализована как древовидная структура, содержащая текстовые определения графических объектов. Высокая скорость обработки строк позволяет поддерживать операции отмены и повтора модификаций хранимых объектов и структуры библиотеки, а также операции переноса элементов между однотипными библиотеками посредством копирования и вставки.

Разработать совершенный строковый класс - найти единственно лучшее решение в рамках оппозиции «универсальность применения - быстродействие» весьма сложно [1]: разным целям и потребностям соответствуют различные проектные решения. В настоящей работе рассматриваются вопросы проектирования оптимальной по быстродействию строковой системы, использующей символьный набор ASCII, в основе реализации которой лежит принцип применения подстроки как универсального аргумента ее функций. Руководство этим принципом позволяет максимально приблизить время выполнения операций сравнения и копирования строк к времени выполнения соответствующих низкоуровневых функций стандартной библиотеки за счет снижения издержек обращения к динамической памяти. В п. 1 и 10 приведены определения классов 8иЬ8'Ьг1^-«подстрока» и 8Б'Ьг1^-«строка» (Swift String). В п. 2-5, 7-9 рассмотрены их ключевые свойства и методы. В п. 3 обоснован выбор низкоуровневых средств стандартной библиотеки, применяемых для сравнения подстрок. В п. 6

отмечены особенности настройки использования встроенных функций компилятора. В п. 11 представлен результат оценки быстродействия спроектированного строкового типа в сравнении с типами, предлагаемыми в библиотеках STL и Qt.

1. Концепция подстроки. Проектируя строковый класс, необходимо учитывать наличие в его составе методов, принимающих в качестве аргументов C-строки, в том числе строковые литералы. Для формирования возвращаемого значения в виде строки такой метод неизбежно должен создать временный объект класса «строка», что при частых вызовах снижает быстродействие приложения за счет издержек обращения к динамической памяти. Кроме того, для таких часто используемых функций строковой системы, как, к примеру, операторы сравнения, как правило, отсутствует возможность передачи фрагмента строки как аргумента. Вместо этого разработчики строковых типов предлагают употреблять специализированные функции с большим числом аргументов (см. функцию STL std::string::compare()), что создает известное неудобство с точки зрения их применения. Эти соображения обосновывают необходимость введения в рамках строковой системы сущности, являющейся ссылкой на строку и хранящей единожды вычисленную длину ее фрагмента, - подстроки. Подстрока известной длины предназначена для применения в качестве универсального временного аргумента для передачи в функции строковой системы, не меняющие содержимого и размещения строк при выполнении (read-only), и возврата из них. Подстрока должна предоставлять возможность выделить собственную подстроку, а также быть сравнима с другими подстроками.

Рассмотрим преимущества наличия в строковой системе подстроки как класса на примере работы функции разбора текста. Анализ текста подразумевает многократный вызов функции, возвращающей очередную лексему для сравнения ее с заданным образцом. Если подстрока не определена, для возврата фрагмента текста формируется новая временная строка (см. реализацию функции STL std::string::substr()). Создание и передача же подстроки по существу представляют собой передачу пары «указатель на позицию в строке - длина подстроки». Далее подстрока может быть использована парсером в операторах сравнения строковой системы как аргумент.

Можем сформировать предварительный состав класса SubString-«подстрока». Этот класс хранит указатель на C-строку с символьным содержимым, свою длину и инкапсулирует ряд методов, в частности функции выделения подстрок, операторы сравнения, взятия символа по индексу:

class SubString { protected:

char *_data;

uint _length;

SubString() { }

SubString &operator=

{ _data = other._data; _length = other._length; return *this; } SubString &operator=

{ _data = (char*)p; _length = strlen(p); return *this; }

public:

SubString(const char *s) : _data((char*)s),_length(strlen(s)) { }

SubString(const char *s, uint l) : _data((char*)s),_length(l) { }

const char *data() const { return _data; }

uint length() const { return _length; }

};

Для обеспечения совместимости проектируемой строковой системы со строковыми типами других разработчиков для каждого из них следует определить функцию, возвращающую подстроку либо ссылку на нее:

inline const SubString &subString(const SomeString &s); inline SubString subString(const SomeString &s);

Функции subString() для подстроки и массива ehar-символов:

inline const SubString &subString(const SubString &s) { return s; } inline SubString subString(const char *p) { return p; }

2. Сравнение подстрок. Строковые типы, возвращающие подстроки, могут использовать функции сравнения вида

template <class T1, class T2>

inline int compare

{ return _compare(subString(s1), subString(s2)); } template <class T1, class T2>

inline int compareEq

{ return _compareEq(subString(s1), subString(s2)); }

Для сравнения подстрок в классе SubString определены перегруженные операторы сравнения (в качестве примера приведены операторы «равно» и «больше-равно»):

class SubString { public:

template <class T>

int operator==(const T &s) const

{ return _compareEq(*this, subString(s)); } template <class T> int operator>=(const T &s) const

{ return _compare(*this, subString(s)) >= 0; }

};

По соображениям компактности кода указанные функции и операторы сравнения реализованы в виде шаблонов, аргументы которых выделяют подстроку. В ином случае было бы необходимо перегружать соответствующие функции для каждого типа аргумента. Введены внешние по отношению к SubString функции, используемые функциями сравнения более высокого уровня:

int _compare

{ return basicCompare(s1, s2); } int _compareEq(const SubString &s1, const SubString &s2) { return basicCompareEq(s1, s2); }

Функция _compare() возвращает целочисленное значение: отрицательное указывает на то, что первый аргумент стоит лексикографически до второго, положительное - после, а нулевое означает их совпадение. Функция _compareEq() возвращает 1, если подстроки имеют равную длину и их символы совпадают, и 0 в ином случае. Следует отметить, что функции _compare() и _compareEq() определяются как не-inline (далее outline). Именно такая форма определения позволяет учитывать в их теле опции, задаваемые компилятору при сборке строковой системы в виде библиотеки. По существу эти функции вводятся как outline-способ вызова базовых функций сравнения:

inline int basicCompare(const SubString &s1, const SubString &s2) { register uint n1 = s1.length(), n2 = s2.length();

if (!(n1 - n2)) return __compare(s1.data(), s2.data(), n1);

if (n1 < n2) n2 = 1;

else { n1 = n2; n2 = (uint)-1; }

return (int)((n1 = __compare(s1.data(), s2.data(), n1)) != 0 ? n1 : n2);

}

inline int basicCompareEq(const SubString &s1, const SubString &s2) { register uint n = s1.length();

return n == s2.length() && !__compare(s1.data(), s2.data(), n);

}

Набор базовых функций сравнения дополняется в соответствии с расширением списка функций более высокого уровня. Базовые функции сравнения обращаются к низкоуровневой функции__compare():

inline int __compare

{ return p1 == p2 ? 0 : memcmp(p1, p2, n); }

Функция__compare(), в свою очередь, использует средства, предоставляемые

стандартной библиотекой для сравнения двух массивов символов. Обоснование выбора функции memcmp() приводится ниже.

3. Сравнение C-строк. C++ унаследовал от C понятие строки как оканчивающегося нулем массива элементов типа char, а также набор функций для манипулирования такими C-строками [1]. Стандартная библиотека для сравнения двух C-строк, длина которых заранее не вычислена, предлагает функцию int strcmp(const char*, const char*). Для сравнения же двух блоков памяти известной длины стандартная библиотека предусматривает использование функции int memcmp(const void*, const void*, size_t), быстродействие которой выше.

Проиллюстрируем разницу во времени работы функций сравнения, предоставляемых стандартной библиотекой, выполнив эксперимент. Отдельно следует оценить время работы функции memcmp() (обозначим ее как memcmpLen()), которой третьим аргументом передается результат вычисления длины C-строки функцией strlen(). Таким образом, мы сможем судить о времени сравнения двух C-строк, длина одной из которых неизвестна, что, в свою очередь, позволит оценить скорость сравнения подстроки (объекта класса SubString) с С-строкой. В случае, когда аргументом strlen() является строковый литерал, встроенная builtin-реализация этой функции

компилятором для расчета длины подразумевает подстановку sizeof(). В результате такой оптимизации время работы шешсшрЬеп() с вычисляемым аргументом длины становится равным времени работы шешсшр(), значения аргументов которой известны заранее. В подтверждение последнего тезиса на графике оценок быстродействия функций для компилятора gcc3.4.5 (рис. 1) приведена кривая шешсшрЬепЬ^, составленная по измерениям времени работы функции шешсшрЬеп(), использующей значения strlen(), аргументами которой являются строковые литералы соответствующей длины.

0.09 0.08 0.07 0.06 0.05 0.04

0.03 0.02 0.01 0

в1ГС1Г

те тещ )Ьеп те: Т1СП11 Ьеп!

тет ;тр

0 5 10 15 20 25 30 35 40 45 50 55 60 65 70

Число символов

Рис. 1. Оценки быстродействия низкоуровневых функций сравнения О-строк ^ее3.4.5)

По оси абсцисс откладывается количество символов в строке. Значение по оси ординат фиксирует время работы функции сравнения в цикле с числом итераций 106 в секундах. Функция сравнивает две совпадающие С-строки (два символьных массива) равной длины - в этом случае время работы максимально. Важно отметить, что цель исследования заключается не в установлении абсолютных значений времени работы функций как таковых, а в соотнесении этих значений друг с другом для получения возможности выбора наиболее быстродействующей.

Из полученных результатов видно, что время работы функции шешсшр() становится меньшим, чем у функции strcmp(), для строк длиной более 8 символов. Для функции же шешсшрЬеп() с вычисляемой длиной второго аргумента эта закономерность обнаруживается для строк длиной более 43 символов. Следует подчеркнуть, что разница во времени работы функций шешсшр() и шешсшрЬеп() обусловлена тем, что в качестве аргументов при определении длины последняя использует массивы символов, а не строковые литералы, по этой причине оптимизацию расчета длины строк компилятор не производит.

Высокая скорость сравнения именно длинных строк имеет значительный практический смысл, поскольку длительное исполнение этой операции - одна из причин существенного снижения быстродействия приложения. Также необходимо отметить, что сравнить два сИаг-массива символов, хотя бы один из которых не завершен нулем, можно только с помощью функции шешсшр(). Исходя из приведенных соображений, можно заключить, что низкоуровневая функция сравнения__сошраге() должна использовать функцию стандартной библиотеки шешсшр().

4. Выделение подстрок подстроки. Для выделения подстроки класс SubString содержит перечень конструкторов в защищенной части. Смысл их наличия состоит в инкапсуляции проверки задаваемых границ, а также в том, чтобы методы SubString, выделяющие подстроки, не использовали конструкторов копирования при возврате значения. Методы SubString, формирующие подстроки, перечислены в публичной части класса (в качестве примера приведены лишь некоторые из них):

class SubString { protected:

// [from, from+l]

SubString(const SubString &ss, uint from, uint l) { register uint n = ss._length; if (from > n) from = n; if ((n -= from) < l) l = n; _data = ss._data + from; _length = l;

}

// [from, to)

SubString(const SubString *ss, uint from, uint to) { // l символов слева

SubString(const SubString &ss, uint l) {

// l символов справа

SubString(uint l, const SubString &ss) {

public:

SubString operator()(uint from, uint l=1) const // [from, from+l]

{ return SubString

{ return SubString(this, from, to); } SubString left(uint l) const // l символов слева

{ return SubString(*this, l); } SubString right(uint l) const // l символов справа

{ return SubString(l, *this); }

};

5. Копирование и конкатенация подстрок. Для реализации возможности копирования подстроки в буфер достаточного размера (его величина не проверяется) предусмотрена последовательность внешних функций:

template <class T>

inline char *copyData(char *dest, const T &s) { return _copyData(dest, subString(s)); }

inline char *__copyData

{ return

{ return __copyData(d, s.data(), s.length()); }

Функция copyData() осуществляет копирование подстроки, взятой от аргумента некоторого типа, в буфер путем вызова функции _copyData() . Функция _copyData(),

... } ... } ... }

в свою очередь, обращается к низкоуровневой функции__copyData(), использующей

функцию стандартной библиотеки memcpy(). Возвращаемым значением выполненной операции является адрес буфера dest. Функция _copyData() введена как outline-

способ вызова функции__copyData(), что позволяет учитывать в ее теле опции,

задаваемые компилятору при сборке строковой системы в виде библиотеки.

При наличии средств копирования возможна реализация операции конкатенации:

template<class T>

inline char *concat(char *dest, const T &s) { return _concat(dest, subString(s)); }

inline char *__concat

{ return __copyData(dest, src, n) + n; }

char *_concat

{ return __concat(dest, s.data(), s.length());

Возврат функцией__concat() указателя на последний символ в буфере dest

позволяет формировать цепочки вложенных вызовов вида

_concat(..._concat(_concat(buf,subStrl),subStr2),...).

Последовательности функций, подобные приведенным выше, названные, скажем, moveData() и unite() (перемещение и объединение), могут быть определены над функцией стандартной библиотеки memmove(), отличной от функции memcpy() в том, что для блоков dest и src допускается наличие общих данных. Поскольку memcpy() не проверяет наличие непустого пересечения блоков памяти, время ее работы меньше.

6. Встроенные функции компилятора. Для повышения скорости работы приложения компилятор GCC позволяет использовать специфические возможности процессора - промежуточный вариант между ассемблером и стандартным языком C - внутренние функции (compiler intrinsics), которые также именуются «встроенными» («builtins») [2]. Такие реализуемые компилятором аналоги функций стандартной библиотеки за счет оптимизации могут оказаться быстрее, к примеру компилятор заменяет вызов встроенной функции strlen() подстановкой sizeof(), если ее аргументом является строковый литерал (см. выполненный выше тест). Однако в зависимости от входных данных и текущей версии компилятора употребление встроенных функций может существенно снижать время работы приложения вместо ожидаемого повышения производительности. Например, время работы builtin memcmp(), сравнивающей два строковых литерала, будет минимальным, в то время как для более часто встречающейся комбинации входных данных оно будет значительно большим по отношению к одноименной функции стандартной библиотеки.

Цель п. 6 не подробное описание преимуществ и недостатков встроенных функций компилятора, а формулировка следующего утверждения: наличие собственной строковой системы позволяет с помощью задания соответствующих опций при сборке библиотеки производить настройку использования компилятором тех или иных встроенных builtin-функций. Опции, задаваемые при сборке, будут учтены для функций, реализованных outline. При дальнейшем применении этой библиотеки в приложениях значения опций ее сборки будут иметь приоритет перед значениями опций сборки приложения.

7. Концепция разделяемого буфера. Оптимизация расходования ресурсов памяти при копировании объектов класса SString реализуется посредством так называемого неявного совместного использования данных (implicit data sharing). Согласно этой концепции (изложенной, к примеру, в [3, с. 283-285]), объекты класса, ассоциированные с одним и тем же значением, разделяют единственное представление - буфер: ссылаются на общую внутреннюю структуру данных в памяти. Этот буфер помимо указателя на данные хранит счетчик ссылающихся на него объектов класса. Преимущество совместного использования заключается в отсутствии необходимости выделения и освобождения памяти при создании и последующем удалении объектов-копий. Идея использования концепции разделяемого буфера при проектировании строковых типов не оригинальна (см. реализацию строк STL, QString). Новизна состоит в употреблении буфера, порожденного от подстроки, что позволяет в полной мере сочетать преимущества обеих концепций.

8. Класс StringBuf (разделяемый буфер строки):

class SString {

friend const SubString &subString(const SString &s); private:

class StringBuf : public SubString {

friend class SString; private:

uint _shares; uint _capacity;

struct Static { static char *sharedEmpty[]; }; static StringBuf *sharedEmpty

{ return (StringBuf*)Static::sharedEmpty; }

char *bufData() { return (char*)(this+1); }

void *operator new(uint sz, uint n);

void *operator new(uint sz, StringBuf *old, uint n);

void operator delete(void *b) { free(b); }

StringBuf() { }

StringBuf(const StringBuf &b) { }

void operator=(const StringBuf &b) { } ~StringBuf() { }

} *_buf;

static StringBuf *use(StringBuf *b) { ++b->_shares; return b; } static void del(StringBuf *b) { if (!b->_shares--) delete(b); }

};

inline const SubString &subString(const SString &s) { return *s._buf; }

Разделяемый буфер StringBuf реализован в качестве потомка подстроки. Таким образом, для употребления строковым типом функций строковой системы, имеющих аргументами подстроки либо типы, для которых определена функция subString(), возвращающая подстроку, достаточно передачи ссылки на собственный буфер.

Класс StringBuf помимо унаследованных свойств содержит поля:

• _shares - количество совместных использований разделяемого буфера. Употреблять выражение «счетчик ссылок» для наименования этого свойства некорректно, поскольку его нулевое значение в описываемой реализации означает ассоциацию буфера с одним объектом SString. Для изменения значения _shares в состав класса SString включены статические методы use() и del();

• _capacity - число символов, вмещаемых буфером без увеличения его размера.

Следует обратить внимание на способ размещения StringBuf в памяти. Разделяемый строковый буфер наследует указатель char *_data на символьные данные. Во избежание двойного вызова функций new и delete, т. е. для снижения издержек обращения к динамической памяти при создании и уничтожении буфера, класс StringBuf перегружает соответствующие операторы.

Оператор new с помощью функции стандартной библиотеки malloc() выделяет блок памяти достаточного размера для размещения самого буфера, заданного числа символов и 0-символа конца строки. В поле StringBuf::_data заносится адрес, вычисляемый функцией StringBuf::bufData() как (char*)(this+1), что равнозначно (char*)this+sizeof(StringBuf), т. е. символьные данные располагаются в выделенном блоке памяти со смещением sizeof(StringBuf). Таким образом, обращения к динамической памяти при создании и уничтожении объекта класса StringBuf будут выполнены по одному разу. Последний элемент массива данных получает значение символа конца строки. Количество совместных использований созданного буфера равно нулю. Для обеспечения возможности изменения размера ранее выделенного блока памяти с помощью функции стандартной библиотеки realloc() в классе StringBuf перегружен оператор new, в список аргументов которого включен адрес буфера.

Для эмуляции разделяемого буфера пустой строки (нулевой длины) класс StringBuf включает структуру, содержащую статический массив указателей на char. Количество элементов массива равно sizeof(StringBuf)/sizeof(char*)+1, т. е. на единицу превосходит число полей StringBuf. В первый элемент заносится адрес последнего, остальные инициализируются нулевым значением. Посредством функции sharedEmpty(), преобразующей указатель, доступ к элементам массива осуществляется с помощью операции -> по именам полей StringBuf. Таким образом, разделяемый буфер пустой строки реализован в виде структуры данных, допускающей статическую инициализацию:

// SString.cpp

char *SString::StringBuf::Static:: sharedEmpty[sizeof(StringBuf) / sizeof(char*)+1] =

{ (char*)sharedEmpty + sizeof(StringBuf) };

9. Безопасное выделение подстрок (механизм блокирования буфера).

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

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

Для реализации механизма блокировки разделяемого буфера вводится промежуточный класс BufLocker, хранящий указатель на разделяемый буфер строки и изменяющий значение его счетчика ссылок в конструкторах и деструкторе. Класс BufLocker применяется в качестве второй базы наследования при определении класса SubStr - немодифицируемой (read-only) подстроки, блокирующей разделяемый буфер. Методы класса SString, выделяющие подстроки (в качестве примера приведены лишь некоторые из них), вызывают соответствующие конструкторы SubStr:

class SString {

private:

class BufLocker; friend class BufLocker; public:

class SubStr; friend class SubStr; operator SubStr() const; // Буфер блокируется.

SubStr operator()(uint from, SubStr segm (uint from,

SubStr left

SubStr right

};

class SString::BufLocker { private:

StringBuf *_buf; protected:

BufLocker(const SString &x) : _buf(use(x._buf)) { } // Блокировать. BufLocker(const BufLocker &x) : _buf(use(x._buf)) { } // ~BufLocker() { del(_buf); } // Разблокировать.

private:

void operator=(const BufLocker&) { }

};

class SString::SubStr : public SubString, public BufLocker {

friend class SString; protected:

SubStr

: SubString(*s._buf, from, l), BufLocker(s) { } // [from, from+l]

SubStr

: SubString

SubStr

: SubString(*s._buf, l), BufLocker(s) { } // l символов слева

SubStr

: SubString(l, *s._buf), BufLocker(s) { } // l символов справа

uint len=1) const; uint to) const;

const; const;

// [from, from+l] // [from, to) // l символов слева // l символов справа

10. Класс SString. Перечислим некоторые ключевые методы и операторы, не упомянутые выше: конструкторы, деструктор, операторы возврата символа по индексу, приведения к С-строке, разыменования, сравнения, присваивания, дозаписи в конец строки:

class SString {

void init(const SubString &s); // Конструирование. SString &set(const SubString &s); // Присваивание. SString &add(const SubString &s); // Добавление. public:

SString() { _buf = use(StringBuf::sharedEmpty()); } SString(const SString &x) { _buf = use(x._buf); }

SString(const char *p, uint n) { init(SubString(p, n)); } SString(const SubString &s) { init(s); } SString(const char *p) { init(SubString(p)); }

~SString() { del(_buf); }

const char &operator[](uint i) const { return _buf->bufData()[i]; } operator const char*() const { return _buf->bufData(); }

const char &operator*() const { return *_buf->bufData(); }

SString &clear

{ del(_buf); _buf = use(StringBuf::sharedEmpty()); return *this; }

// Сравнение. template <class T>

inline int operator>=(const T &s) const

{ return _compare(*_buf, subString(s)) >= 0; }

// Присваивание.

SString &operator=

{ StringBuf *t = _buf; _buf = use(s._buf); del(t); return *this; } SString &operator=(const SubString &s) { return set(s); } SString &operator=(const char* s) { return set(subString(s)); }

// Добавление в конец. template <class T>

SString &operator<<(const T &s) { return add(subString(s)); }

};

// SString.cpp

void SString::init

{ _copyData((_buf = new(s.length())StringBuf)->bufData(), s); } // Присвоить/дописать данные, по необходимости переразместив буфер. SString &SString::set(const SubString &s) { ... } SString &SString::add(const SubString &s) { ... }

11. Демонстрация быстродействия. Предположим, что для задач разбора текста требуется функция, сравнивающая два строковых объекта, которыми могут являться строки, их фрагменты и строковые литералы. Оценить скорость выполнения операций сравнения описываемой строковой системы в соотнесении со строковыми системами библиотек STL и Qt можно, выполнив эксперимент, суть которого заключается в измерении времени работы такой функции. Предлагаемый тест является комплексным, поскольку помимо времени работы функций и операторов сравнения на результат также окажут влияние характерные для строковой системы издержки передачи строковых аргументов сравнивающей функции.

Функция _stringCmp() для сравнения двух подстрок использует оператор «больше-равно». Функция stringCmp() производит вызов _stringCmp() в цикле с числом итераций 106, передавая фрагмент глобально созданной строки и строковый литерал с соответствующим строковой системе преобразованием к типу подстроки. Аргументы представляют совпадающие наборы символов - время их сравнения максимально.

Приведем код участвующих в тесте функций. Оценки быстродействия продемонстрированы на рис. 2 и 3, где по оси абсцисс откладывается количество символов в строке, значение по оси ординат фиксирует время работы функции stringCmp() в секундах. Для наглядности на графиках представлен результат измерения быстродействия функции memcmp(), сравнивающей массивы символов соответствующей длины.

t, с

О 5 10 15 20 25 30 35 40 45 50 55 60 65 70

Число символов

Рис. 2. Оценки быстродействия выполнения операций сравнения строк ^ее3.4.5)

Значения ординат точек кривой шешсшр отмечают наименьшее возможное время сравнения символьных массивов, достижимое через посредство использования низкоуровневых функций стандартной библиотеки. Эксперимент выполнялся для компиляторов gcc3.4.5 и gcc4.4.0:

typedef void caller(); clock_t start, stop; uint i, r, cmpNum = 1000000; double cmpTime = 0;

О 5 10 15 20 25 30 35 40 45 50 55 60 65 70

Число символов

Рис. 3. Оценки быстродействия выполнения операций сравнения строк ^ее4.4.0)

// Символьный массив некоторого содержания.

char _string[] = "It's a long way to the promise land...";

void meter(caller **callers, uint len) { start = clock(); callers[len](); stop = clock();

cmpTime += ((stop - start)/(double)CLOCKS_PER_SEC);

}

#define doCompare for(i =0; i < cmpNum; ++i) ///////////////////////// SString ////////////////////////// SString sStr(_string);

inline void _sStringCmp(const SubString &si, const SubString &s2) { r = si >= s2; }

void sStringCmp0() { doCompare _sStringCmp(sStr.left(0), ""); } void sStringCmpi() { doCompare _sStringCmp(sStr.left(1), "I"); } void sStringCmp2() { doCompare _sStringCmp(sStr.left(2), "It"); }

caller *sStringCmpCallers[] = { sStringCmpO, sStringCmpi, sStringCmp2, ... }; //////////////////////// STL String //////////////////////// std::string stlStr(_string);

inline void _stlStringCmp(const std::string &si, const std::string &s2) { r = si >= s2; }

void stlStringCmp0() { doCompare _stlStringCmp(stlStr.substr(0, 0), ""); } void stlStringCmpi() { doCompare _stlStringCmp(stlStr.substr(0, i), "I"); } void stlStringCmp2() { doCompare _stlStringCmp(stlStr.substr(0, 2), "It"); }

caller *stlStringCmpCallers[] = { stlStringCmp0, stlStringCmpi, stlStringCmp2,

///////////////////////// QString ////////////////////////// QString qStr(_string);

inline void _qStringCmp

{ doCompare _qStringCmp

{ doCompare _qStringCmp

{ doCompare _qStringCmp(qStr.leftRef(2), QStringO'It'O.leftRef^)); }

caller *qStringCmpCallers[] = { qStringCmpO, qStringCmpi, qStringCmp2, ... }; //////////////////////////////////////////////////////////// int main(int argc, char *argv[]) {

for (uint i = 0, _len = strlen(_string); i < _len+1; ++i) { meter(sStringCmpCallers, i); meter(stlStringCmpCallers, i); meter(qStringCmpCallers, i);

}

return 0;

}

Наихудший результат строковой системы STL объясняется отсутствием в ней класса «подстрока». По этой причине при формировании фрагмента строки всякий раз требуется создавать объект класса std::string, что значительно снижает общее быстродействие приложения за счет издержек обращения к динамической памяти.

Лучшая по отношению к STL оценка быстродействия выполнения операций сравнения в строковой системе Qt демонстрирует преимущества использования концепции подстроки, однако проведенный тест в то же время обнаруживает недостатки ее реализации, а именно невозможность конструирования Qt-подстроки непосредственно от строкового литерала, которая, в свою очередь, обусловливает необходимость создания временного объекта QString.

Разница в оценках скорости работы функций сравнения SString для двух компиляторов отражает отличие реализаций их встроенных builtin-функций strlen(): компилятор gcc4.4.0 при конструировании подстроки оптимизирует вычисление длины строкового литерала подстановкой sizeof(), сближая тем самым кривые memcmp и SString.

Заключение. Рассмотрены ключевые аспекты проектирования быстродействующей ASCII строковой системы, опирающейся в своей реализации на принцип использования подстроки в качестве универсального аргумента своих функций. Представлен результат теста, демонстрирующий превосходство разработанного строкового типа по быстродействию выполнения операций сравнения строк по отношению к аналогам, предложенным в библиотеках STL и Qt.

Автор благодарит Д. В. Калинина за помощь при обсуждении рассматриваемых в статье вопросов.

Литература

1. Страуструп Б. Язык программирования C+—спец. изд. / пер. с англ. С. Анисимова, М. Кононова; под ред. Ф. Андреева, А. Ушакова. М.: ООО «Бином-Пресс», 2004. 1104 с.

2. Кохарчик Д., Джонс К. Внутренние функции компилятора GCC для обработки данных в векторной форме / пер. с англ. А. Панина. [Электронный ресурс] URL: http://www.

rus-linux.net/MyLDP/algol/gcc-compiler-intrinsics-vector-processing

3. Бланшетт Ж., Саммерфилд М. Qt4: программирование GUI на C+—h 2-е изд., доп. / пер. с англ. С. Лунина, В. Казаченко. М.: Кудиц-Пресс, 2008. 736 с.

References

1. Stroustrup Bjarne. Yazik programmirovaniya C+ + [The C+—h Programming Language: special edition]. Per. s angl. S. Anisimova, M. Kononova; pod red. F. Andreeva, A. Ushakova. Moscow, OOO "Binom-Press", 2004, 1104 p.

2. Koharchik George, Jones Kathy. Vnutrennie funktsii kompilyatora GCC dlya obrabotki dannyh v vektornoy forme [An Introduction to GCC Compiler Intrinsics in Vector Processing]. Per. s angl. A. Panina. Available at: http://www.rus-linux.net/MyLDP/algol/gcc-compiler-intrinsics-vector-processing (accessed by 20.12.2013).

3. Blanchette Jasmin, Summerfield Mark. Qt4: programmirovaniye GUI на C++ [C+—h GUI Programming with Qt4]. 2nd edition, dop. Per. s angl. S. Lunina, V. Kazachenko. Moscow, Kudits-Press, 2008, 736 p.

Статья рекомендована к печати проф. В. Ю. Добрыниным. Статья поступила в редакцию 17 февраля 2014 г.