<?xml version='1.0' standalone='no' ?>
<journal xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="JournalArticulus.xsd">
    <operCard>
        <operator>ИПМ им. М.В.Келдыша РАН|Publicator</operator>
        <date>2026-04-08 19:34:28</date>
        <cntArticle>1</cntArticle>
        <cs>0</cs>
    </operCard>
    <titleid>32018</titleid>
    <issn>2071-2901</issn>
    <codeNEB>20712901</codeNEB>
    <journalInfo lang="RUS">
        <title>Препринты ИПМ им. М.В.Келдыша</title>
    </journalInfo>
    <issue>
        <volume/>
        <number>72</number>
        <altNumber/>
        <part/>
        <dateUni>2025</dateUni>
        <issTitle/>
        <pages>1-21</pages>
        <articles>
            <article>
                <pages>1-21</pages>
                <artType>RAR</artType>
                <authors>
                    <author num="001">
                        <authorCodes>
                            <orcid>0009-0000-1026-4788</orcid>
                        </authorCodes>
                        <individInfo lang="RUS">
                            <surname>Котельников</surname>
                            <initials>Алексей Михайлович</initials>
                            <orgName>Институт прикладной математики им. М.В.Келдыша РАН</orgName>
                            <email>kotelnikov@imamod.ru</email>
                        </individInfo>
                        <individInfo lang="ENG">
                            <surname>Kotelnikov</surname>
                            <initials>A.M.</initials>
                            <orgName>Keldysh Institute of Applied Mathematics RAS</orgName>
                        </individInfo>
                    </author>
                </authors>
                <artTitles>
                    <artTitle lang="RUS">О вычислительной эффективности типов данных для последовательностей при хранении и обработке нерегулярных сеток</artTitle>
                    <artTitle lang="ENG">On the Computational Efficiency of Data Types for Sequences in Storage and Processing Irregular Grids</artTitle>
                </artTitles>
                <abstracts>
                    <abstract lang="RUS">Излагаются основные свойства структур данных для последовательностей, таких как массивы, стеки, очереди и связные списки. Разработаны новые типы для коллекций на языке C++ с использованием "любознательного" рекурсивного шаблона, позволяющего избегать копирования кода между классами. Проведено сравнение производительности структур данных из стандартной библиотеки C++, из кода MARPLE и собственных разработок.</abstract>
                    <abstract lang="ENG">The main properties of data structures for sequences, such as arrays, stacks, queues, and linked lists, are outlined. New types for collections in C++ have been developed using a curious recursive template to avoid code duplication between classes. A performance comparison has been conducted among the data structures from the C++ standard library, the MARPLE code, and proprietary developments.</abstract>
                </abstracts>
                <text lang="RUS">   
1 Введение
   
   При программной реализации дискретных сеточных моделей сплошной среды естественным образом возникает необходимость хранения информации о соответствии между элементами сетки и относящимися к ним физическими величинами. Первоначально, в эпоху становления теории разностных схем, в вычислительной практике доминировали разнообразные варианты схем на регулярных сетках. Способ хранения таких сеток был простым: элемент двумерного (или трехмерного) массива очевидным образом ассоциировался с узлом (или ячейкой) прямоугольной сетки. На самом деле даже в столь простом случае вопрос не столь очевиден, как кажется: при погружении тела нетривиальной формы в прямоугольную область некоторые ячейки должны были быть объявлены неактивными, что приводило к неполному использованию прямоугольного массива для хранения сеточных данных.
   Использование сеток нерегулярной структуры, получившее чрезвычайно широкое распространение, привело к необходимости хранения информации о структуре самой сетки, помимо сеточных величин. Для хранения таких сеток разрабатывались различные структуры данных [1, 2], оптимизированные как для стационарных, так и для нестационарных сеток. В то же время языки программирования, такие как Фортран и С, не предоставляли разработчикам каких-либо сложных структур данных для хранения последовательностей объектов - единственной структурой для хранения последовательности оставался массив, а при необходимости хранения динамических последовательностей (то есть предполагающих эффективную реализацию удаления, вставки и перемещения подпоследовательности) можно было использовать самостоятельно реализованные связные списки. Соответственно, структуры для хранения сеток так или иначе основывались на массивах (или, реже, на связных списках).
   Появление языка С++ и традиции его использования для решения вычислительных задач породили новые возможности для хранения и обработки сеток и сеточных величин. В основном здесь имеется в виду стандартная библиотека шаблонов STL, содержащая классы контейнеров, объединенных общим интерфейсом. Использование того или иного контейнера обычно считается вопросом чисто технического, программистского уровня, и поэтому обычно если и обсуждается, то только в узком кругу разработчиков. Обоснование выбора того или иного контейнера с точки зрения оптимизации производительности стало очень нетривиальной задачей. На эффективность реализации теперь также влияет сложная архитектура современных процессоров: из-за наличия разноуровневой кэш-памяти принято считать, что производительность вычислений резко возрастает в случае компактного размещения данных в адресном пространстве памяти; это обстоятельство сильно снизило популярность динамических структур типа связных списков.
   В настоящей работе приводятся результаты тестирования производительности различных контейнеров: как стандартных из библиотеки STL, так и
   
некоторых разработанных и используемых авторами в ходе работы над кодом MARPLE [2].

2   Общие сведения о структурах данных, исследуемых в данной работе
   
   Перечислим основные рассматриваемые структуры данных. Динамический массив хранит данные подряд в едином участке динамически выделенной памяти.
Статический массив хранит данные в теле класса подобно динамическому массиву, не обращаясь при этом к динамической памяти. Связный список хранит данные в нодах - специальном классе, хранящем элемент вместе с указателем на следующий и, опционально, предыдущий элемент.
   
  Далее рассмотрим структуры данных, разработанные ранее для пакета прикладных программ MARPLE.
Кластерный массив - односвязный список указателей на массивы одинакового фиксированного размера.
Односвязный список с глобальным пулом нод является вариацией од-носвязного списка, но память выделяется не отдельно для каждого элемента, а массивом, содержащим память под константное число нод.
   
  Теперь перечислим разработанные в рамках статьи структуры данных. Расщепленный массив хранит данные в участках памяти постоянного размера. Сами указатели на участки хранятся в динамическом массиве. Каскадный массив устроен аналогично расщепленному массиву, но размер каждого участка памяти в два раза больше предыдущего. Фенвиков массив разностей - структура данных, представляющая собой дерево Фенвика, но, в отличие от него, хранящая вместо самих элементов их разности во внутреннем динамическом массиве. Причиной разработки явилась необходимость прибавлять константу к идущим подряд элементам в некотором диапазоне индексов и делать это за логарифмическое от числа элементов время.
Связный список на внутреннем массиве - вариант связного списка, где элементы хранятся во внутренней структуре данных.

В таблице 1 представлены основные асимптотики.

3   Свойства структур данных

Рассмотрим теоретические аспекты работы структур данных.

3.1   Статический массив
   Массив, максимальный размер которого заранее известен. Благодаря этому хранимые объекты размещаются в теле класса, что позволяет не расходовать динамическую память, избегая фрагментирования и уменьшая за
   
траты времени на создание массива, что особенно актуально для временных структур, предназначенных для хранения данных в пределах одной функции. Объекты сохраняют свои позиции при добавлении новых элементов, но перемещаются вместе с переменной, представляющей массив, что усложняет использование этой структуры данных в качестве вложенной в другие структуры, так как указатели на элемент вложенного статического массива могут потерять свой объект, на который обязаны указывать. В С++ эта структура данных реализована в библиотеке boost [3], а также будет включена в стандартную библиотеку нового стандарта С++26 [4].

3.2   Динамический массив
  Динамический массив хранит объекты в непрерывном участке памяти, который может быть заполнен как частично, так и полностью. При добавлении элемента в полностью заполненный массив происходит расширение участка памяти, что может сопровождаться перемещением объектов на новое место. Очевидно, такая операция имеет временную сложность O(N). Для того, чтобы удержать среднюю сложность времени заполнения в пределах O(N), нужно обеспечить достаточно редкое перемещение объектов на новое место. Это можно гарантировать, если заполненный участок памяти расширяется не менее, чем в а &gt; 1 раз, где а — коэффициент расширения.
   Существенным недостатком динамического массива является возможность смены адресов объектов при изменении вместимости, либо дефрагментации памяти.

3.2.1   Выбор коэффициента расширения
   При формировании рекомендаций по выбору будем исходить из следующих соображений. Во-первых, новый участок должен быть выделен до того, как будет освобожден старый. Таким образом, оба участка должны быть размещены в памяти, и лишь затем один из них будет освобожден и пригоден для очередного выделения памяти. Это требование связано как с отсутствием такой функциональности в стандартных библиотеках, так и с проблемами при перемещении нетривиальных классов на новые адреса. Если же есть возможность двигать границы текущего участка памяти, то коэффициент расширения можно выбирать свободно. Во-вторых, класс, реализующий динамический массив, отвечает лишь за себя, поэтому рассматривается процесс заполнения одного динамического массива, не конкурирующего за динамическую память с другими потребителями.
а
полняется элементами, последовательно увеличивая вместимость от начальной единичной до финальной ап: 1, а, а2, а3, ..., ап, затем выполняется операция сокращения и дефрагментации, представленная в С++ методом shrink_to_fit, пройдя всего n расширений.

  Для того чтобы итоговый участок памяти мог быть размещен на месте первого, должно быть выполнено условие
п / П-1  г    ап - 1
ап &lt; &gt;  аг = .
" z=0        а - 1

а &gt; 1
тации памяти после заполнения динамического массива:

а&lt; 2,
   
   
  Коэффициент расширения должен быть строго меньше двойки. При этом число расширений, которые должен пройти массив, прежде чем дефрагмен-тация станет возможной, составляет не менее
n0 = - loga (2 - а). (2)
Рассмотрим иной сценарий расширения: 1 а, а2, а3, ... , а п
требуем, чтобы участок размера ап-\ расширяясь до ап, встал на позицию исходного участка единичного размера.
n^v2  г    ап-1 - 1
г=0
а &gt; 1

"&lt;*•     1        .где Ф = 1±^ и 1.618. (3)
аП-1 &gt; т+Ь. 2
  Длина цикла с возвратом на исходную позицию составляет при этом не менее
no = 1 - loga (1 + а - а2). (4)
  Таким образом, можно сформулировать рекомендации по подбору коэффициента расширения:
• при наличии возможности двигать границы участка памяти коэффициент можно выбирать произвольным образом;

ции;
• при коэффициенте менее золотого сечения (~ 1.618: корень уравнения Ф2 = Ф + 1) новый участок памяти периодически будет оказываться на исходной позиции не менее чем за (4) шагов, снижая общее потребление памяти программой;
• при коэффициенте, не превышающем сверхзолотое сечение (~ 1.466: корень уравнения а3 = а2 + 1), получаем цикл длины 4;
• 
• при коэффициенте, не превышающем пластическое число (~ 1.325: корень уравнения а3 = а + 1), получаем цикл длины 3;
•
этому стоит обратить особое внимание на те из коэффициентов, которые наиболее удобны для выполнения операции целочисленного двоичного умножения и деления. К таким значениям можно отнести 1.25 с циклом длины 3 и коэффициент 1.5 с циклом длины 5.
3.3 Расщепленный массив
   Расщепленный массив представляет собой массив указателей на неподвижные подмассивы фиксированной длины, размещаемые в динамической памяти. Доступ к элементу осуществляется путем деления номера запрашиваемого элемента на размер подмассива. Поскольку данная операция является довольно тяжеловесной, то в качестве размера подмассива допускается либо степень двойки, либо, в случае преаллоцированной первой страницы, заранее известная на момент компиляции константа. Деление на константу компилятор потенциально может заменить операцией умножения на обратное к константе число, представленное в двоичном виде периодической дробью. Такой подход позволяет избегать вызова непосредственно операции целочисленного деления с остатком.
3.4 Каскадный массив
   Каскадный массив представляет собой модификацию расщепленного массива, когда каждый следующий подмассив оказывается в два раза больше предыдущего. При таком подходе уменьшается число задействованных участков памяти, однако доступ к элементу усложняется.
   Пусть требуется получить доступ к элементу I, что означает необходимость вычислить номер подмассива Р и смещение элемента F. При размере начального подмассива s получаем условие принадлежности элемента I к подмассиву Р:

P-1 P

i=0 i=0
   Вычисляя суммы геометрических прогрессий, получаем в итоге систему неравенств:
                       s(2P - 1) &lt; /&lt;s(2P+1 - 1). (5) Решая систему неравеств, получаем ограничения на Р:

P &lt; log2 (1 + 1) &lt;P + 1.

Округляя вниз до ближайшего целого, получаем итоговые формулы.

P = Li°g2 Ц s J + 1)J, (е)
F = I - s(2P - 1). W
   Формулы (6) содержат две вычислительно непростые операции: целочис-
ленное деление и целочисленный логарифм по основанию два. Первая из них
может быть оптимизирована наложением тех же условий на размер началь-
ного подмассива, что и в случае расщепленного массива. Для целочисленно-
го логарифма по основанию два можно использовать специальную команду
процессора BSR (Bit Scan Reverse), которая позволяет найти номер самого
старшего из выставленных бит целого числа. В С++ данную команду можно
использовать, воспользовавшись семейством интринсик _BitScanReverse для
Visual Studio и builtin_clz для GNU Compiler Collection.
  Альтернативный способ поиска позиции элемента без использования интринсик предполагает обход всех подмассивов от последнего к первому с проверкой выполнения условий (5). Поскольку размеры подмассивов удваиваются, последний подмассив содержит более половины от общей вместимости структуры данных, что обеспечивает довольно высокую вероятность получить адрес нужного элемента уже на первой итерации поиска, если в каскадном массиве нет пустых подмассивов.

3.5   Фенвиков массив разностей
  Такой массив представляет собой структуру данных, предназначенную для оптимизации одной конкретной операции массового прибавления константы к непрерывному подмассиву исходной последовательности целых чисел при сохранении приемлемого времени доступа к произвольному элементу.
3.5.1 Дерево Фенвика
   За основу взята структура данных, именуемая деревом Фенвика, обеспечивающая логарифмическое время для операций доступа к элементу и суммы любого подмассива [6, 5]. Если в эту структуру помещать не сами элементы, а разности двух соседних элементов исходной последовательности, то получим новую структуру данных, на которой остановимся подробнее.
3.5.2 Теоретическое обоснование
   Пусть задана исходная последовательность целых чисел, нумеруемая с единицы а1,&lt;2дг. Примем а0 = 0. Определим функцию lsb (Least Significant Bit — наименьший значащий бит) — наибольший делитель положительного целого числа, являющийся степенью двойки. Для этой функции выполняются ключевые условия, что при вычитании или прибавлении к числу его
   
собственного lsb для получившегося числа возрастает не менее, чем в два раза:

lsb(x ± lsb(x)) &gt; 2 • lsb(x). (7)
   Фенвиков массив разностей хранит последовательность bi = ai - ai-isb(i). Для получения элемента исходной последовательности требуется выполнить суммирование
ai = bi + bi-isb(i) + bi-isb(i)-isb(i-isb(i)) + ...
. Из свойства удвоения шага (7) следует, что количество слагаемых будет O(logN).
   Фенвиков массив разностей предназназначен для выполнения быстрой операции прибавления константы к подряд идущим элементам. Пусть требуется прибавить константу к подряд идущим элементамa1...an. Для этого требуется изменить лишь те хранимые элементы bi, для которых i - lsb(i) &lt; n &lt; i. Такими элементами являются лишь bi,bi+isb(i),bi+isb(i)+isb(i+isb(i)),количество которых тоже O(logN).


4   Реализация структур данных
   
   Перечисленные структуры данных реализуют общепринятый в С++ интерфейс стандартной библиотеки шаблонов.

4.1   "Любознательный" рекурсивный шаблон
   Интерфейс стандартной библиотеки шаблонов С++ требует от любого класса наличия большого количества методов, нередко повторяющихся. Для обеспечения повторного использования кода применяется подход, известный как "любознательный рекурсивный шаблон" (The Curiously Recurring Template Pattern) [7].
   Рассмотрим этот подход на примере методов, отвечающих за получение итератора. Помимо методов begin и end константной и неконстантной реализации в классе требуется ряд аналогичных методов, отвечающих за получение итератора. Поскольку реализация таких методов одинакова для многих классов, возникает желание иметь для этого кода одно единственное место во всей программе. Реализуется это желание следующим образом:
   
#define  ME ((S&amp;)*this)#define  MEС   ((const S&amp;)*this)template&lt;class  S&gt;   struct   iteratable {INCLUDE_SUBTYPES(S);const_iterator   cbeginO   const  {  return MEC.beginO;}const_iterator   cend()   const   {  return  MEC.endO; }reverse_iterator  rbeginO {return  std::make_reverse_iterator(ME.end()) ;}const_reverse_iterator   crbeginO   const {return  std::make_reverse_ iterator(cend());}const_reverse_iterator  rbeginO   const  {  return crbegin();}reverse_iterator  rend () {return  std:: make_re ver se_ it erat or (ME.beginO);}const_reverse_iterator   crendO   const {return  std:: make_re ver se_ it erat or (cbeginO);}const_reverse_iterator  rend ()   const  {  return crendO;}};   Рассмотрим некий класс iteratable, зависящий от класса S, передаваемого ему как аргумент шаблона. Это несамостоятельная структура данных, играющая вспомогательную роль, поэтому назовем его класс-кусочек. В противовес этому, класс S будем назвать главным классом. Класс-кусочек через макросы ME и МЕС (неконстантная и константная ссылки соответственно) получает доступ к методам, которые предоставляют главный класс или другие классы-кусочки, подключенные к главному классу. В процессе компиляции выполняется проверка, что все методы и поля, запрашиваемые одним классом-кусочком, предоставляются другим классом-кусочком, либо непосредственно главным классом.
Для подключения класса-кусочка, требуется выполнить несколько шагов.
   Во-первых, нужно определить все типы заранее отдельно от главного класса my_type. Позже макрос INCLUDE_SUBTYPES(S) позволит быстро их загрузить в любой ваш класс.
template&lt;class  Т&gt;   struct  my_type; template&lt;class T&gt;
struct   subtypes_of&lt;my_type&lt;T&gt;&gt; { using  value_type   = T; using  size_type   =  std : :size_t ; using  difference_type   =  std::ptrdiff_t; using  reference   = value_type&amp;; using  const_reference   =  const value_type&amp;; using  pointer  = value_type*; using  const_pointer  =   const value_type*; using  iterator  = pointer; using  const_iterator  =   const_pointer;
using  reverse_iterator  =  std::reverse_iterator&lt;iterator&gt;;
using  const_reverse_iterator  =   std::reverse_iterator&lt;const_iterator&gt;;
};

   Во-вторых, реализовать сам главный класс, подключив классы-кусочки и определив необходимые для их работы методы.
template&lt;class Т&gt; struct  my_type :
public   iteratable&lt;my_type &lt;T&gt;&gt;
{
   INCLUDE.SUBTYPES(my.type &lt;T&gt;); private :
   //   class implementation public:
iterator  beginO   {  /*method   implementation*/ } iterator  end ()   {  /*method  implementation*/ }
const_iterator  beginO   const  {  /*method  implementation*/ } const_iterator  end()   const  {  /*method  implementation*/ }
};
   Класс-кусочек iteratable содержит 8 методов, которые включаются в главный класс при помощи наследования. Для доступа к этим методам из главного класса потребуется приведение типа *this к iteratable &lt; my_type &lt; T &gt;&gt;7 однако снаружи все эти методы доступны пользователю главного класса безо всяких затруднений.
   При помощи "любознательного" рекурсивного шаблона в классы-кусочки можно вынести любые наборы методов, общие для нескольких разных главных классов, избегая тем самым копирования кода между разными частями программы, повышая общую надежность, не затрагивая при этом производительность программы.
5   Замеры производительности 5.1 Методика
   Измерена операция сортировки случайной последовательности элементов, сгенерированной стандартной операцией std::srand(l). Сортировка выполнена функцией std::sort для массивов (таблица 2) и специальным методом sort для списков (таблица 3), а также собственной реализацией метода sort для списка из MARPLE. Также измерена операция прибавления константы к первой трети элементов. Обе замеряемые операции достаточно сложны, чтобы избежать избыточной оптимизации всей программы, которая может затронуть простые операции, такие как простое суммирование всех элементов. Общий размер структуры выбран 227 восьмибайтовых чисел.
  Для сброса кэша между замерами применяется операция выделения новой страницы и последующего освобождения. Сама операция не требует много времени, однако обязывает операционную систему дважды обновить список страниц процесса. Все замеры проводятся на одном запуске во избежание влияния возможных различий между узлами суперкомпьютера. Каждый результат был получен последовательностью из 60 замеров (10 для сортировки списков). Перед началом последовательности один замер выполнялся вхолостую для того, чтобы программа получила от операционной системы всю
  
необходимую память, а также подгрузила все необходимые динамические библиотеки.


Результаты замеров

5.2 Выводы
Результаты представлены в таблицах 2 и 3.
   Кластерный массив MARPLE уступает по производительности стандартным структурам данных, а также имеет быстрорастущие асимптотики.
   Односвязный список MARPLE по времени итерирования превосходит стандартные списки благодаря глобальному кэшу нод. Это демонстрирует эффективность кэширования при оптимизации структур данных. При этом собственный метод сортировки уступает стандартным реализациям при отсутствии кэширования. Следует ожидать значительного снижения эффективности односвязного списка MARPLE по мере перемешивания нод в памяти в ходе активной модификации списков, однако в настоящей работе такой эксперимент не проводился.
   Фенвиков массив разностей выполняет задачу прибавления константы на несколько порядков быстрее других структур, однако платой за это служит замедление времени доступа примерно в 40 раз.
   Расщепленный и каскадный массивы демонстрируют производительность, уступающую аналогичной структуре данных из стандартной библиотеки std::deque.
Автор благодарит А. С. Болдарева за плодотворное сотрудничество.

</text>
                <codes>
                    <udk>004.4</udk>
                    <edn>JVGEZU</edn>
                </codes>
                <keywords>
                    <kwdGroup lang="RUS">
                        <keyword>динамический массив</keyword>
                        <keyword>дерево Фенвика</keyword>
                        <keyword>"любознательный" рекурсивный шаблон</keyword>
                    </kwdGroup>
                    <kwdGroup lang="ENG">
                        <keyword>dynamic array</keyword>
                        <keyword>Fenwick tree</keyword>
                        <keyword>curiously recurring template pattern</keyword>
                    </kwdGroup>
                </keywords>
                <references>
                    <reference>Карташева Е.Л. Об одном подходе к разработке систем подготовки и анализа данных для решения прикладных задач математической физики // М.: ИММ РАН, 1993, №38</reference>
                    <reference>Гасилов В.А., Болдарев А.С., Ольховская О.Г.,Бойков Д.С., Шарова Ю.С., Савенко Н.О., Котельников А.М. MARPLE: программное обеспечение для мультифизического моделирования в задачах сплошных сред // Препринты ИПМ им. М.В. Келдыша. 2023. № 37. 41 с. DOI: https://doi.org/10.20948/prepr-2023-37.</reference>
                    <reference>Josuttis N.M. Boost.Array // [Электронный ресурс]. — URL: https://www.boost.org/doc/libs/master/libs/array/doc/html/array.html (дата обращения: 21.11.2025).</reference>
                    <reference>cppreference.net. Inplace Vector // [Электронный ресурс]. — URL:  https://cppreference.net/cpp/container/inplace_vector.html (дата обращения: 21.11.2025).</reference>
                    <reference>Fenwick P.M. A new data structure for cumulative frequency tables // Software: Practice and Experience. 1994. Т. 24, № 3. С. 327–336.</reference>
                    <reference>Рябко Б. Быстрый последовательный код // Доклады АН СССР. 1989. Т. 306, № 3. С. 548—552. URL: https://boris.ryabko.net/dan1989.pdf (дата обращения: 21.11.2025).</reference>
                    <reference>Abrahams David, Gurtovoy Aleksey. C++ Template Metaprogramming: Concepts, Tools, and Techniques from Boost and Beyond // 2004. 400 с. ISBN 0-321-22725-5.</reference>
                </references>
                <files>
                    <furl>https://keldysh.ru/papers/2025/prep2025_72.pdf</furl>
                </files>
            </article>
        </articles>
    </issue>
</journal>
