Skip to content

Instantly share code, notes, and snippets.

@Maccimo
Created March 7, 2016 05:36
Show Gist options
  • Save Maccimo/15cd1e189536d797066b to your computer and use it in GitHub Desktop.
Save Maccimo/15cd1e189536d797066b to your computer and use it in GitHub Desktop.
--- 278509.html 2016-03-07 08:32:37.670799200 +0300
+++ 278509-asya.html 2016-03-07 08:33:22.384356700 +0300
@@ -1,79 +1,54 @@
<div class="post_show">
<div class="post shortcuts_item" id="post_278509">
- <div class="published">вчера в 18:39</div>
+ <div class="published"> 3 марта в 18:39</div>
<h1 class="title">
- <span class="post_title">Шустрый потокобезопасный менеджер кучи и голый Си</span>
+ <span class="post_title">Шустрый потокобезопасный менеджер кучи, Ася и голые Си</span>
</h1>
<div class="hubs">
<a href="https://habrahabr.ru/hub/programming/" class="hub subscribed" title="Вы подписаны на этот хаб">Программирование</a><span class="profiled_hub" title="Профильный хаб">*</span>,
<a href="https://habrahabr.ru/hub/parallel_programming/" class="hub subscribed" title="Вы подписаны на этот хаб">Параллельное программирование</a><span class="profiled_hub" title="Профильный хаб">*</span>,
- <a href="https://habrahabr.ru/hub/algorithms/" class="hub subscribed" title="Вы подписаны на этот хаб">Алгоритмы</a><span class="profiled_hub" title="Профильный хаб">*</span>,
<a href="https://habrahabr.ru/hub/cpp/" class="hub subscribed" title="Вы подписаны на этот хаб">C++</a><span class="profiled_hub" title="Профильный хаб">*</span>,
- <a href="https://habrahabr.ru/hub/c/" class="hub subscribed" title="Вы подписаны на этот хаб">C</a><span class="profiled_hub" title="Профильный хаб">*</span>
+ <a href="https://habrahabr.ru/hub/c/" class="hub subscribed" title="Вы подписаны на этот хаб">C</a><span class="profiled_hub" title="Профильный хаб">*</span>,
+ <a href="https://habrahabr.ru/hub/asm/" class="hub subscribed" title="Вы подписаны на этот хаб">Assembler</a><span class="profiled_hub" title="Профильный хаб">*</span>
</div>
<div class="content html_format">
- текст обновлен 04.03.15 20:36<br>
+ текст обновлен 07.03.16 9:22<br>
<br>
При планировании любой задачи мы стремимся как можно точнее конкретизировать запросы, определить исходные данные и по возможности избавиться от любой неопределенности, мешающей просчитать конечный результат. Однако при разработке высокоуровневой логики не всегда уделяется внимание таким простым казалось бы вещам, как размещение данных в памяти, менеджмент потоков, обрабатывающих наш функционал, особенности реализации динамических массивов или бинарный интерфейс процедур. Когда написанная программа предельно лаконична и оптимизирована, но при этом работает не так быстро, как хотелось бы, закономерно возникает вопрос: «а что еще можно улучшить?» Насколько можно доверять низкоуровневому инструментарию, написанному профессиональными программистами, безусловно разбирающимися в своем деле, но при этом ни черта не понимающими в тех идеях, что вы хотите реализовать? Фрагментация, зацикленность, прерывания, события, объекты, уведомления, каждое новое знакомство с Си-шными или WinAPI-шными библиотеками подталкивает к очевидной мысли: «зачем такая громоздкая реализация?» Почему нельзя просто сделать менеджер кучи выделяющий память за строгое количество шагов? Использовать real-time статистику при работе с разделяемыми данными, вместо сложной системы семафоров и уведомлений? Наращивать динамические массивы без переразмещения и обращаться к случайной ячейке за одинаковое время после любого количества реформаций? Миссия не кажется невыполнимой. Самое время поэкспериментировать.<br>
<br>
-Предлагаю широкому вниманию процедурную реализацию менеджера кучи, способного выделять диапазоны памяти заказных размеров. Цикл поиска при этом не превышает нормированных пределов. Работа с памятью состоит всего из двух процедур: memAlloc и memRecycle, выполняющих связанные функции. Потокобезопасность поддерживается с помощью дополнительного инструментария, в свою очередь состоящего еще из нескольких процедур. О распараллеливании немного поподробнее: принцип базируется на ассоциированной блокировке разделяемых данных, схожих с блокировкой шины методами Interlocked, однако без блокировки шины. Весь процесс сводится к созданию позиционных стопоров, являющихся по сути диапазонами памяти, содержащими однобайтовые метки состояний каждого из потоков в доступном пуле. Размер выводится из этого соотношения. Поскольку я использую восьмипотоковый пул (а больше мне не нужно), то позиционные стопоры у меня занимают 8 байт (64 бита). Перед перезаписью разделяемой информации стопор блокируется исполняющим потоком, записывая метку в байт под смещением своего номера в пуле. Другие потоки не будут работать с разделяемыми данными пока стопор не обнулится, выполняя Sleep, либо откладывая задачу, либо считая овечек в цикле, на выбор программиста.<br>
+Предлагаю широкому вниманию процедурную реализацию менеджера кучи, способного выделять диапазоны памяти заказных размеров. Цикл поиска при этом не превышает нормированных пределов. Работа с памятью состоит всего из двух процедур: memAlloc и memRecycle, выполняющих связанные функции. Потокобезопасность поддерживается с помощью дополнительного инструментария, в свою очередь состоящего еще из нескольких процедур. О распараллеливании немного поподробнее: принцип базируется на ассоциированной блокировке разделяемых данных, схожих с блокировкой шины методами Interlocked, однако без блокировки шины. Выбор такого подхода прост — блокировка шины работает немного медленнее. Весь процесс сводится к созданию позиционных стопоров, являющихся по сути диапазонами памяти, содержащими однобайтовые метки состояний каждого из потоков в доступном пуле. Размер выводится из этого соотношения. Поскольку я использую восьмипотоковый пул (а больше мне не нужно), то позиционные стопоры у меня занимают 8 байт (64 бита). Перед перезаписью разделяемой информации стопор блокируется исполняющим потоком, записывая метку в байт под смещением своего номера в пуле. Другие потоки не будут работать с разделяемыми данными пока стопор не обнулится, выполняя Sleep, либо откладывая задачу, либо считая овечек в цикле, на выбор программиста.<br>
<br>
-<b>Забегая немного вперед опубликую результаты тестов новоизобретенных узлов велосипеда в сравнении со стандартными malloc/free</b><br>
-Запустим в цикле процедуру запроса/возврата отрезков памяти разных размеров, используя стандартные средства замера времени GetTickCount(). Дабы избежать обнуления сверхмалых интервалов промасштабируем цикл до 10000 шагов, чтобы оценивать время в миллисекундах.<br>
-<a name="habracut"></a><br>
-<pre><code class="cpp hljs"><span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">long</span> StartTime;
-<span class="hljs-keyword">unsigned</span> <span class="hljs-keyword">long</span> Elapsed;
-INT64 testPtr[<span class="hljs-number">1000</span>];
-INT64 switcher = <span class="hljs-number">1</span>;
-INT64 counter = <span class="hljs-number">0</span>;
-
-StartTime = GetTickCount();
-<span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">100</span>; i++) {
- <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i2 = <span class="hljs-number">0</span>; i2 &lt; <span class="hljs-number">100</span>; i2++) {
- <span class="hljs-keyword">if</span> (switcher &gt; <span class="hljs-number">0</span>) {
- <span class="hljs-comment">//testPtr[i2] = malloc(i2 * 1024 + 64); //путем переключения строк тестируем каждый метод</span>
- testPtr[i2] = memAlloc(<span class="hljs-number">0</span>, i2 * <span class="hljs-number">1024</span> + <span class="hljs-number">64</span>, <span class="hljs-number">0</span>);
- }
- <span class="hljs-keyword">else</span> {
- <span class="hljs-comment">//free(testPtr[i2]);</span>
- memRecycle(<span class="hljs-number">0</span>, testPtr[i2], <span class="hljs-number">0</span>);
- }
- counter++;
- }
- switcher *= <span class="hljs-number">-1</span>;
-}
-Elapsed = GetTickCount() - StartTime;
-</code></pre><br>
-<br>
-<b>результаты:</b><br>
-пятикратный цикл malloc/free: 1954, 1906, 2375, 1953, 1891 = в среднем 2015 миллисекунд<br>
-пятикратный цикл memAlloc/memRecycle: 16, 15, 16, 16, 16 = в среднем 16 миллисекунд<br>
-<br>
-Довольно ощутимая разница особенно ярко проявляет себя при работе с огромными потоками данных, поступающих с разных направлений одновременно. Оптимизация казалось бы рутинного процесса аллоцирования памяти оказывается особенно актуальной при работе с логическими и векторными сетями, когда разнотипные данные, принадлежащие связанным множествам не могут быть записаны в массивы. Размещение таких данных осуществляется иерархически и каждый диапазон модифицируется, переразмещается и удаляется независимо. Только представьте себе чтение текста из 70 тыс строк и сохранение его логической структуры и вы сразу поймете, что процедура распределения памяти — это основа. Тот фундамент на котором должен строиться грамотный алгоритм.<br>
+Забегая немного вперед скажу что в сравнении со стандартными malloc/free, новоизобретенным колесам велосипеда удается работать быстрее от нескольких десятков до сотни раз. Оценить результаты многопоточных тестов можно в конце статьи. Довольно ощутимая разница особенно ярко проявляет себя при работе с масштабными потоками данных, поступающих с разных направлений одновременно. Оптимизация казалось бы рутинного процесса аллоцирования памяти оказывается особенно актуальной при работе с логическими и векторными сетями, когда разнотипные данные, принадлежащие связанным множествам не могут быть записаны в последовательные массивы. Размещение таких данных осуществляется иерархически и каждый диапазон модифицируется, переразмещается и удаляется независимо. Только представьте себе чтение текста из 70 тыс строк и сохранение его логической структуры и вы сразу поймете, что процедура распределения памяти — это основа. Тот фундамент на котором должен строиться грамотный алгоритм.<br>
<br>
<b>Особенности реализации</b><br>
-Основной особенностью алгоритма выделения памяти в данном менеджере является перераспределение вычислений. Часть операций, необходимых для поиска последовательного диапазона внутри фрагментированной памяти, выполняется как предподготовка при запуске функции отработки memRecycle. Идея этого заключена в делении возвращаемых отрезков на группы размерности, количество которых определяется программистом на этапе запуска процедуры-конструктора. Именно цепочка отработки проверяется первой при запросе нового диапазона. Именно количество групп размерности является величиной цикла поиска перед выделением нового диапазона. memRecycle ищет группу размерности, соответствующую размеру запрошенного отрезка + 1, после чего берет указатель на первый в цепочке диапазон, поскольку он безусловно больше запрашиваемого на одну ступень. При отсутствии в цепочках отработки диапазона необходимого размера, память берется из самой большой верхней группы, в которой хранится неразмеченый запас, величина которого также указывается при запуске конструктора. Иными словами для получения одного отрезка длины не менее запрошенной, обработчик делает однократный цикл с фиксированным количеством шагов (по группам размерности) в поисках отработанных диапазонов и берет первый попавшийся нужной размерности + 1.<br>
+Основной особенностью алгоритма выделения памяти в данном менеджере является перераспределение вычислений. Часть операций, необходимых для поиска последовательного диапазона внутри фрагментированной памяти, выполняется как предподготовка при запуске функции отработки memRecycle. Идея этого заключена в делении возвращаемых отрезков на группы размерности, количество которых определяется программистом на этапе запуска процедуры-конструктора. Именно цепочка отработки проверяется первой при запросе нового диапазона. Именно количество групп размерности является величиной цикла поиска перед выделением нового диапазона. memAlloc ищет группу размерности, соответствующую размеру запрошенного отрезка + 1, после чего берет указатель на первый в цепочке диапазон, поскольку он безусловно больше запрашиваемого на одну ступень. При отсутствии в цепочках отработки диапазона необходимого размера, память берется из самой большой верхней группы, в которой хранится неразмеченый запас, величина которого также указывается при запуске конструктора. <br>
+<br>
+Таким образом для получения одного отрезка длины не менее запрошенной, обработчик делает однократный цикл с фиксированным количеством шагов (по группам размерности) в поисках отработанных диапазонов и берет первый попавшийся нужной размерности + 1. Для возврата отрезка обратно memRecycle выполняет еще меньше действий, всего лишь удлиняя цепочку нужной группы размерности еще одним указателем. Без циклов. Это нехитрой последовательностью действий достигается высокая производительность: что такое для современных процессоров цикл из двух сотен проверок опустошения указателя, когда они способны исполнять циклы из миллионов шагов менее чем за 1 миллисекунду.<br>
<br>
<img src="https://habrastorage.org/files/746/c59/c0a/746c59c0a8fc4385adcd05ce783759cb.jpg"><br>
<br>
Реализация потокобезопасности является вариацией на тему программно-транзакционной памяти и осуществляется ступенчатым процессом, представляющим собой одну неразрывную логическую последовательность. Условие для получения эксклюзивного доступа к разделяемому диапазону памяти достигается тривиальной установкой битовой метки в логически связанный позиционный стопор, в котором для каждого потока из пула выделен собственный байт. ASM-процедура перед обращением к разделяемой памяти проверяет стопор на нуль, при соответствии записывая метку в байт со смещением номера потока. После чего стопор подвергается проверке по маске, в которой должен присутствовать только один бит под нужным смещением. Несоответствие означает одновременное редактирование стопора параллельными потоками, процедура возвращает нуль. Каждый поток, получивший нуль при попытке перезаписи разделяемых данных уходит в цикл ожидания, реализация которого может подбираться на свой вкус, после чего следует повторная попытка. При соответствии маски процедура возвращает единичку, что свидетельствует об эксклюзивном доступе к данным.<br>
<br>
-Листинг ассемблерной процедуры (стоит отметить, что при работе вне пула, в потоке не имеющем номера, все же используется блокировка шины):<br>
-<cut><br>
+Стоит отметить, что при работе вне пула, в потоке не имеющем номера из пула, все же используется блокировка шины. Важным моментом в свете этого решения является количество потоков, допущенных для работы с разделяемыми данными. Поскольку в данной реализации оно ограничено оптимальным с точки зрения операционных издержек числом 8, а также используются стопоры размером 8х8=64 бита, то все инструкции используют qword ptr, для чтения или обработки. Создание оперативного пула для выполнения задач требует минимальной настройки исходных данных, а именно: присвоение уникальных номеров от нуля до значения workPoolMax, определяемого программистом и согласуемого с размером стопоров. Если планируется доступ к разделяемым данным вне пула, например из main-потока, висящего в ожидании пользовательского ввода или других, необходимо присвоить им номера меньше нуля (или больше workPoolMax, с добавлением соответствующих условий в процедуры) и вместе с этим сократить размер рабочего пула, чтобы не превышать общее кол-во потоков, допущенных к работе с разделяемыми данными.<br>
+<br>
+<img src="https://habrastorage.org/files/921/a74/4a2/921a744a2e0446728f01b9ae8ae51a16.jpg"><br>
+<br>
+Листинг ассемблерной процедуры:<br>
+<a name="habracut"></a><br>
<code>cnsBit PROC ;check and set bit<br>
cmp rdx,0 ;первичная проверка на нумерацию потока<br>
jl @negtv ;отрицательные значения не относятся к нумерованному пулу и обрабатываются через InterlockedExchange<br>
<br>
cmp qword ptr [rcx],0 ;проверка метки на нуль<br>
@@ -81,36 +56,42 @@
<br>
mov r8,rdx ;подготовка номера потока для смещения<br>
mov rax,8 ;подготовка множителя, биты устанавливаются в начале каждого байта<br>
mul rdx ;умножение для получения итогового смещения<br>
<br>
-mov byte ptr [rcx+r8],1 ;установка бита по нужному адресу<br>
+mov byte ptr [rcx+r8],1 ;установка значения по нужному адресу<br>
bts rdx,rax ;установка бита в маске по необходимому смещению<br>
<br>
cmp qword ptr [rcx],rdx ;проверка метки по маске<br>
jnz @end ;выход при несоответствии<br>
<br>
@scess: ;блок соответствия<br>
mov rax,1 ;собственный бит присутствует в диапазоне, сторонние биты отсутствуют<br>
ret<br>
+<br>
@end: ;блок несоответствия<br>
- mov byte ptr [rcx+r8],0 ;удаление бита из метки<br>
+ mov byte ptr [rcx+r8],0 ;обнуление байта в стопоре<br>
+<br>
@fin: ;блок безусловного выхода<br>
mov rax,0 ;отсутствует собственный бит, либо присутствуют сторонние биты<br>
ret<br>
<br>
@negtv: ;блок для ненумерованных потоков<br>
xor rax,rax ;сравниваемый операнд равен нулю<br>
+ mov rdx, -1 ;стопор будет заполнен битами по всему объему<br>
lock cmpxchg qword ptr[rcx],rdx ;compare exchange<br>
jnz @fin<br>
jmp @scess<br>
cnsBit ENDP<br>
</code><br>
<br>
Клоны Interlocked процедур — threadExchange, threadCompareExchange и threadExchangeAdd выполняют те же функции, что и оригиналы. Наверно их реализация не заслуживает внимания, однако кому-то возможно пригодится:<br>
<cut><br>
-<pre><code class="cpp hljs">threadRepeatCounter[<span class="hljs-number">7</span>]; <span class="hljs-comment">//счетчик повторов</span>
+<pre><code class="cpp hljs">threadRepeatCounterArray = [<span class="hljs-number">9</span>];
+*threadRepeatCounter = &amp;threadRepeatCounterArray[<span class="hljs-number">1</span>] ; <span class="hljs-comment">//счетчик повторов</span>
+INT64 threadSleepTimer = <span class="hljs-number">1000</span>;
+
<span class="hljs-function">INT64 <span class="hljs-title">threadExchangeAdd</span><span class="hljs-params">(INT64 threadNum, INT64 *counter, INT64 *stopper, INT64 value)</span> <span class="hljs-comment">//итерация счетчиков. переданное значение прибавляется к находящемуся в целевом диапазоне, изначальное возвращается.</span>
</span>{
INT64 ret = <span class="hljs-number">0</span>;
<span class="hljs-keyword">for</span> (;;) {
<span class="hljs-keyword">if</span> (cnsBit((INT64)stopper, threadNum) == <span class="hljs-number">1</span>) {
@@ -118,11 +99,12 @@
*counter += value;
*stopper = <span class="hljs-number">0</span>;
<span class="hljs-keyword">break</span>;
}
threadRepeatCounter[threadNum] += <span class="hljs-number">1</span>;
- Sleep(<span class="hljs-number">1</span>);
+ <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; threadSleepTimer * (threadNum + <span class="hljs-number">1</span>); i++)
+ Sleep(<span class="hljs-number">0</span>);
}
<span class="hljs-keyword">return</span> ret;
}
@@ -135,11 +117,12 @@
*pointer = value;
*stopper = <span class="hljs-number">0</span>;
<span class="hljs-keyword">break</span>;
}
threadRepeatCounter[threadNum] += <span class="hljs-number">1</span>;
- Sleep(<span class="hljs-number">1</span>);
+ <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; threadSleepTimer * (threadNum + <span class="hljs-number">1</span>); i++)
+ Sleep(<span class="hljs-number">0</span>);
}
<span class="hljs-keyword">return</span> ret;
}
@@ -154,11 +137,12 @@
}
*stopper = <span class="hljs-number">0</span>;
<span class="hljs-keyword">break</span>;
}
threadRepeatCounter[threadNum] += <span class="hljs-number">1</span>;
- Sleep(<span class="hljs-number">1</span>);
+ <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; threadSleepTimer * (threadNum + <span class="hljs-number">1</span>); i++)
+ Sleep(<span class="hljs-number">0</span>);
}
<span class="hljs-keyword">return</span> ret;
}
</code></pre><br>
<br>
@@ -191,15 +175,16 @@
INT64 pInvalidAddress = <span class="hljs-number">0</span>;
INT64 *baseMemStore = <span class="hljs-number">0</span>;
INT64 *memRecycleStore = <span class="hljs-number">0</span>; <span class="hljs-comment">//служебный диапазон указателей на размеченную память</span>
INT64 *memRecyclePoint = <span class="hljs-number">0</span>; <span class="hljs-comment">//точки входа в цепочки блоков указателей</span>
-INT64 memRecycleBlock = <span class="hljs-number">0</span>; <span class="hljs-comment">//указатель на крайний выделенный блок в общем диапазоне</span>
+INT64 memRecycleBlock = <span class="hljs-number">0</span>; <span class="hljs-comment">//указатель на цепочку блоков отработки</span>
INT64 memClear = <span class="hljs-number">0</span>; <span class="hljs-comment">//модификатор для побайтового обнуления выделенного отрезка</span>
INT64 memNoDefrag = <span class="hljs-number">0</span>; <span class="hljs-comment">//модификатор для переработки без дефрагментации</span>
INT64 memBlockLimit = <span class="hljs-number">1000</span>; <span class="hljs-comment">//максимальный лимит блока для отработанных отрезков памяти</span>
-INT64 memFragmentCount = <span class="hljs-number">0</span>; <span class="hljs-comment">//количество фрагментированных отрезков памяти</span>
+INT64 memFragmentCountArray[<span class="hljs-number">9</span>]; <span class="hljs-comment">//количество фрагментированных отрезков памяти для статистики</span>
+INT64 *memFragmentCount = &amp;memFragmentCountArray[<span class="hljs-number">1</span>];
INT64 memGroupLimit = <span class="hljs-number">200</span>; <span class="hljs-comment">//лимит групп размерностей (предел цикла поиска перед размещением)</span>
INT64 memStoreSize = <span class="hljs-number">1024</span> * <span class="hljs-number">256</span> * <span class="hljs-number">4096</span>; <span class="hljs-comment">//предельный размер кучи</span>
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">memConstruct</span><span class="hljs-params">()</span> </span>{
GetSystemInfo(&amp;sSysInfo);
@@ -217,12 +202,12 @@
INT64 workPtr = <span class="hljs-number">0</span>;
INT64 *chainPtr = <span class="hljs-number">0</span>;
INT64 tempVal = <span class="hljs-number">0</span>;
INT32 sizeGroup = <span class="hljs-number">0</span>;
- <span class="hljs-keyword">if</span> (size &lt; <span class="hljs-number">3200</span>)
<span class="hljs-comment">//проверка нижнего подмножества малых отрезков</span>
+ <span class="hljs-keyword">if</span> (size &lt; <span class="hljs-number">3200</span>)
sizeGroup = size / <span class="hljs-number">32</span>;
<span class="hljs-comment">//проверка верхнего подмножества больших отрезков</span>
<span class="hljs-keyword">else</span> sizeGroup = size / (<span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>) + <span class="hljs-number">100</span>;
groupSearch:
@@ -253,31 +238,34 @@
chainPtr = memRecyclePoint[sizeGroup];
<span class="hljs-keyword">if</span> ((INT64)chainPtr == <span class="hljs-number">0</span>)
<span class="hljs-keyword">goto</span> getStore;
}
chainRead:
- <span class="hljs-comment">//проверка соответствия числа помещенных в блок указателей и извлеченных</span>
- <span class="hljs-keyword">if</span> (chainPtr[<span class="hljs-number">-3</span>] &lt; chainPtr[<span class="hljs-number">-2</span>]) {
+ <span class="hljs-comment">//проверка соответствия числа помещенных в блок указателей и извлеченных с контролем превышения</span>
+ <span class="hljs-keyword">if</span> (chainPtr[<span class="hljs-number">-3</span>] &lt; chainPtr[<span class="hljs-number">-2</span>] &amp;&amp; chainPtr[<span class="hljs-number">-3</span>] &lt; memBlockLimit) {
workPtr = threadExchange(threadNum, &amp;chainPtr[chainPtr[<span class="hljs-number">-3</span>]], &amp;chainPtr[<span class="hljs-number">-1</span>], <span class="hljs-number">0</span>);
<span class="hljs-comment">//итерация счетчика извлеченных указателей</span>
threadExchangeAdd(threadNum, &amp;chainPtr[<span class="hljs-number">-3</span>], &amp;chainPtr[<span class="hljs-number">-1</span>], <span class="hljs-number">1</span>);
<span class="hljs-keyword">if</span> ((INT64)workPtr == <span class="hljs-number">0</span>)
<span class="hljs-keyword">goto</span> chainRead;
<span class="hljs-comment">//фрагментация отрезка памяти</span>
<span class="hljs-comment">//несоответствие доступного размера отрезка и заявленного требует дополнительной обработки</span>
tempVal = *(INT64*)(workPtr - <span class="hljs-number">8</span>);
- <span class="hljs-comment">//если взятый из отработки отрезок слишком велик, процедура образует из него два отрезка. Ненужный будет возвращен в свою группу размерности.</span>
+ <span class="hljs-comment">//Если отрезок слишком велик, он делится на два. Остаток помещается в свою группу размерности.</span>
<span class="hljs-keyword">if</span> (tempVal - size &gt; <span class="hljs-number">3200</span>) {
*(INT64*)(workPtr + size + <span class="hljs-number">24</span>) = tempVal - size - <span class="hljs-number">32</span>;
*(INT64*)(workPtr + tempVal) = (INT64)workPtr + size + <span class="hljs-number">32</span>;
*(INT64*)(workPtr + tempVal + <span class="hljs-number">8</span>) = *(INT64*)(workPtr + tempVal + <span class="hljs-number">16</span>) = <span class="hljs-number">0</span>;
<span class="hljs-comment">//отделенный лишний отрезок уходит на переработку</span>
memRecycle(threadNum, (INT64)workPtr + size + <span class="hljs-number">32</span>, &amp;memNoDefrag);
}
- <span class="hljs-comment">//поскольку размер найденного отрезка может оказаться незначительно больше заявленного, происходит индексирование</span>
+ <span class="hljs-comment">//размер найденного необработанного отрезка может оказаться больше заявленного, происходит индексирование.</span>
<span class="hljs-keyword">else</span> size = *(INT64*)(workPtr - <span class="hljs-number">8</span>);
+
+<span class="hljs-comment">//индексирование количества фрагментированных отрезков для статистики</span>
+memFragmentCount[threadNum]--;
}
<span class="hljs-comment">//если рабочие указатели в данный момент отсутствуют, отрезок берется из запаса</span>
<span class="hljs-keyword">else</span> <span class="hljs-keyword">goto</span> getStore;
}
<span class="hljs-comment">//если группы подходящей размерности отсутствуют, новый отрезок берется из запаса</span>
@@ -324,47 +312,58 @@
<span class="hljs-keyword">if</span> (param == &amp;memNoDefrag)
<span class="hljs-keyword">goto</span> recycle;
<span class="hljs-comment">//дефрагментация освобожденных ранее отрезков</span>
<span class="hljs-keyword">for</span> (;;) {
- <span class="hljs-comment">//сверка указателей на предыдущий отрезок и блок отработки</span>
- <span class="hljs-keyword">if</span> (*(INT64*)(workPtr - <span class="hljs-number">16</span>) == <span class="hljs-number">0</span> || **(INT64**)(workPtr - <span class="hljs-number">16</span>) != *(INT64*)(workPtr - <span class="hljs-number">32</span>))
+ <span class="hljs-comment">//сверка указателей блока отработки и начала текущего отрезка. при соответствии отрезок считается отработанным.</span>
+ tempPtr = *(INT64*)(workPtr - <span class="hljs-number">16</span>);
+ <span class="hljs-keyword">if</span> ((INT64)tempPtr == <span class="hljs-number">0</span> || *tempPtr != *(INT64*)(workPtr - <span class="hljs-number">32</span>))
<span class="hljs-keyword">break</span>;
- tempVal = threadExchange(threadNum, *(INT64*)(workPtr - <span class="hljs-number">16</span>), *(INT64*)(workPtr - <span class="hljs-number">24</span>) - <span class="hljs-number">8</span>, <span class="hljs-number">0</span>);
+ tempVal = threadExchange(threadNum, (INT64)tempPtr, &amp;memRecycleStore[<span class="hljs-number">1</span>], <span class="hljs-number">0</span>);
<span class="hljs-comment">//если указатель на отрезок в блоке отработки был обнулен в параллельном потоке, процедура завершает поиск</span>
<span class="hljs-keyword">if</span> (tempVal == <span class="hljs-number">0</span>)
<span class="hljs-keyword">break</span>;
<span class="hljs-comment">//при объединении отрезков, размер текущего прибавляется к следующему ниже в диапазоне</span>
tempSize = *(INT64*)(workPtr - <span class="hljs-number">8</span>);
workPtr = *(INT64*)(workPtr - <span class="hljs-number">32</span>);
+
*(INT64*)(workPtr - <span class="hljs-number">8</span>) += (tempSize + <span class="hljs-number">32</span>);
+
+<span class="hljs-comment">//индексирование количества фрагментированных отрезков для статистики</span>
+memFragmentCount[threadNum]--;
}
<span class="hljs-comment">//итоговый размер обрабатываемого отрезка дополнительно сохраняется после всех преобразований</span>
tempSize = *(INT64*)(workPtr - <span class="hljs-number">8</span>);
+
<span class="hljs-keyword">for</span> (;;) {
<span class="hljs-comment">//проверка заполнения следующего блока</span>
- <span class="hljs-keyword">if</span> (*(INT64*)(workPtr + tempSize + <span class="hljs-number">24</span>) == <span class="hljs-number">0</span>)
+ tempVal = *(INT64*)(workPtr + tempSize + <span class="hljs-number">24</span>);
+ <span class="hljs-keyword">if</span> (tempVal == <span class="hljs-number">0</span>)
<span class="hljs-keyword">break</span>;
<span class="hljs-comment">//размер впереди идущего отрезка прибавляются сразу, все дальнейшие вычисления идут с необходимым смещением.</span>
- tempSize += *(INT64*)(workPtr + tempSize + <span class="hljs-number">24</span>) + <span class="hljs-number">32</span>;
- <span class="hljs-comment">//сверка указателей на следующий отрезок и блок отработки</span>
- <span class="hljs-keyword">if</span> (*(INT64*)(workPtr + tempSize + <span class="hljs-number">16</span>) == <span class="hljs-number">0</span> || **(INT64**)(workPtr + tempSize + <span class="hljs-number">16</span>) != *(INT64*)(workPtr + tempSize))
+ tempSize += tempVal + <span class="hljs-number">32</span>;
+ tempPtr = *(INT64*)(workPtr + tempSize + <span class="hljs-number">16</span>);
+ <span class="hljs-comment">//сверка указателей блока отработки и начала текущего отрезка. при соответствии отрезок считается отработанным.</span>
+ <span class="hljs-keyword">if</span> ((INT64)tempPtr == <span class="hljs-number">0</span> || *tempPtr != *(INT64*)(workPtr + tempSize))
<span class="hljs-keyword">break</span>;
- tempVal = threadExchange(threadNum, *(INT64*)(workPtr + tempSize + <span class="hljs-number">16</span>), *(INT64*)(workPtr + tempSize + <span class="hljs-number">8</span>) - <span class="hljs-number">8</span>, <span class="hljs-number">0</span>);
+ tempVal = threadExchange(threadNum, (INT64)tempPtr, &amp;memRecycleStore[<span class="hljs-number">1</span>], <span class="hljs-number">0</span>);
<span class="hljs-comment">//если указатель на отрезок в блоке отработки был обнулен в параллельном потоке, процедура завершает поиск</span>
<span class="hljs-keyword">if</span> (tempVal == <span class="hljs-number">0</span>)
<span class="hljs-keyword">break</span>;
<span class="hljs-comment">//при объединении заранее проиндексированный размер фиксируется в обрабатываемом отрезке</span>
*(INT64*)(workPtr - <span class="hljs-number">8</span>) = tempSize;
+
+<span class="hljs-comment">//индексирование количества фрагментированных отрезков для статистики</span>
+memFragmentCount[threadNum]--;
}
-recycle:
<span class="hljs-comment">//настройка блоков указателей</span>
+recycle:
<span class="hljs-comment">//итоговый размер обрабатываемого отрезка дополнительно сохраняется после всех преобразований</span>
tempSize = *(INT64*)(workPtr - <span class="hljs-number">8</span>);
- <span class="hljs-keyword">if</span> (tempSize &lt; <span class="hljs-number">3200</span>)
<span class="hljs-comment">//проверка нижнего подмножества малых отрезков</span>
+ <span class="hljs-keyword">if</span> (tempSize &lt; <span class="hljs-number">3200</span>)
sizeGroup = tempSize / <span class="hljs-number">32</span>;
<span class="hljs-comment">//проверка верхнего подмножества больших отрезков</span>
<span class="hljs-keyword">else</span> sizeGroup = tempSize / (<span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>) + <span class="hljs-number">100</span>;
<span class="hljs-comment">//превышение пределов размерностей переводит указатель на крайнюю группу</span>
sizeGroup = (sizeGroup &gt; memGroupLimit) ? memGroupLimit : sizeGroup;
@@ -392,10 +391,17 @@
<span class="hljs-comment">//итерация счетчика добавления указателей. указатель на отрезок будет добавлен в блок под полученным номером.</span>
tempVal = threadExchangeAdd(threadNum, &amp;tempPtr[<span class="hljs-number">-2</span>], &amp;tempPtr[<span class="hljs-number">-1</span>], <span class="hljs-number">1</span>);
<span class="hljs-comment">//если блок переполнен, создается новый</span>
<span class="hljs-keyword">if</span> (tempVal &gt; memBlockLimit)
<span class="hljs-keyword">goto</span> createBlock;
+
+ <span class="hljs-comment">//отработка отрезка</span>
+ <span class="hljs-comment">//после необходимых преобразований указатель на начало отрезка модифицируется</span>
+ *(INT64*)(workPtr + tempSize) = workPtr;
+ <span class="hljs-comment">//указатель на блок отработки и позицию для сброса при дальнейшей дефрагментации</span>
+ *(INT64*)(workPtr + tempSize + <span class="hljs-number">8</span>) = (INT64)tempPtr;
+ *(INT64*)(workPtr + tempSize + <span class="hljs-number">16</span>) = &amp;tempPtr[tempVal];
<span class="hljs-comment">//запись указателя на отработанный отрезок памяти в блок под нужным номером</span>
tempPtr[tempVal] = workPtr;
<span class="hljs-comment">//реструктурирование цепочки групп размерности</span>
<span class="hljs-comment">//при создании нового блока указателей, необходимо встроить его в цепь</span>
@@ -408,78 +414,137 @@
}
<span class="hljs-comment">//указатель на новый блок помещается в точку входа, если она обнулена изначально, либо за время процедурных действий</span>
<span class="hljs-keyword">if</span> (memRecyclePoint[sizeGroup] == <span class="hljs-number">0</span>)
memRecyclePoint[sizeGroup] = (INT64)tempPtr;
- <span class="hljs-comment">//финализация</span>
- <span class="hljs-comment">//после всех преобразований указатель на начало отрезка модифицируется</span>
- *(INT64*)(workPtr + tempSize) = workPtr;
- <span class="hljs-comment">//указатель на блок отработки и позицию для сброса при дальнейшей дефрагментации</span>
- *(INT64*)(workPtr + tempSize + <span class="hljs-number">8</span>) = (INT64)tempPtr;
- *(INT64*)(workPtr + tempSize + <span class="hljs-number">16</span>) = &amp;tempPtr[tempVal];
+<span class="hljs-comment">//индексирование количества фрагментированных отрезков для статистики</span>
+memFragmentCount[threadNum]++;
}
</code></pre><br>
<cut><br>
<pre><code class="cpp hljs"><span class="hljs-function">INT64 <span class="hljs-title">memPtrCheck</span><span class="hljs-params">(INT64 *ptr)</span> <span class="hljs-comment">//процедура проверки адреса на соответствие диапазону памяти кучи</span>
</span>{
<span class="hljs-keyword">if</span> ((<span class="hljs-keyword">unsigned</span> __int64)ptr &gt; (<span class="hljs-keyword">unsigned</span> __int64)baseMemStore &amp;&amp; (<span class="hljs-keyword">unsigned</span> __int64)ptr &lt;= (<span class="hljs-keyword">unsigned</span> __int64)*baseMemStore)
<span class="hljs-keyword">return</span> *ptr;
<span class="hljs-keyword">return</span> &amp;pInvalidAddress;
}
-</code></pre>
+</code></pre><br>
+<br>
+<b>Многопоточное тестирование</b><br>
+Чтобы выяснить как менеджер ведет себя в более реальных условиях запустим цикл из 10000 шагов с выделением отрезков памяти случайного размера, генерируемого функцией rand(). Напишем процедуру timeTestSub() и запустим в восьми потоках одновременно. Точное время выполнения попробуем замерить методами QueryPerformanceCounter/Frequency. Дабы быть уверенным, что потоки честным образом выполнялись одновременно используем счетчики пересечений, фиксирующие количество перезапусков threadExchange-процедур. А чтобы наблюдать сложность структуры и фрагментированность кучи используем счетчик цепи, показывающий количество фрагментов, находящихся в отработке.<br>
+<br>
+<pre><code class="cpp hljs">LARGE_INTEGER timerFrequency;
+<span class="hljs-keyword">double</span> timeResult[<span class="hljs-number">8</span>];
+<span class="hljs-keyword">double</span> timeSum = <span class="hljs-number">0</span>;
+INT64 testPtr[<span class="hljs-number">8</span>][<span class="hljs-number">100</span>];
+HANDLE ghEvents[<span class="hljs-number">8</span>];
+HANDLE hHeap;
+INT64 testStpr = <span class="hljs-number">0</span>;
+INT64 testVal = <span class="hljs-number">0</span>;
+INT64 repeatCounter = <span class="hljs-number">0</span>;
+INT64 fragmentCounter = <span class="hljs-number">0</span>;
+
+<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">timeTestSub</span><span class="hljs-params">(INT64 threadNum)</span>
+</span>{
+ INT64 switcher = <span class="hljs-number">1</span>;
+ LARGE_INTEGER timerStart;
+ LARGE_INTEGER timerStop;
+ QueryPerformanceCounter(&amp;timerStart);
+
+ <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">100</span>; i++) {
+ <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i2 = <span class="hljs-number">0</span>; i2 &lt; <span class="hljs-number">100</span>; i2++) {
+ <span class="hljs-keyword">if</span> (switcher &gt; <span class="hljs-number">0</span>) {
+ testPtr[threadNum][i2] = memAlloc(threadNum, rand(), <span class="hljs-number">0</span>);
+ <span class="hljs-comment">//testPtr[threadNum][i2] = malloc(rand()); //путем переключения строк тестируем каждый метод</span>
+ <span class="hljs-comment">//testPtr[threadNum][i2] = (INT64)HeapAlloc(hHeap, 0, rand());</span>
+ }
+ <span class="hljs-keyword">else</span> {
+ memRecycle(threadNum, testPtr[threadNum][i2], <span class="hljs-number">0</span>);
+ <span class="hljs-comment">//free(testPtr[threadNum][i2]);</span>
+ <span class="hljs-comment">//HeapFree(hHeap, 0, (LPVOID)testPtr[threadNum][i2]);</span>
+ }
+ }
+ switcher *= <span class="hljs-number">-1</span>;
+ }
+
+ QueryPerformanceCounter(&amp;timerStop);
+ timeResult[threadNum] = timerStop.QuadPart - timerStart.QuadPart;
+ timeResult[threadNum] = timeResult[threadNum] / timerFrequency.QuadPart;
+ SetEvent(ghEvents[threadNum]);
+}
+
+<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span>
+</span>{
+ hHeap = HeapCreate(<span class="hljs-number">0</span>, <span class="hljs-number">1024</span> * <span class="hljs-number">256</span> * <span class="hljs-number">4096</span>, <span class="hljs-number">0</span>);
+ QueryPerformanceFrequency(&amp;timerFrequency);
+ <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">8</span>; i++)
+ ghEvents[i] = CreateEvent(<span class="hljs-literal">NULL</span>, FALSE, FALSE, <span class="hljs-literal">NULL</span>);
+ <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">8</span>; i++) {
+ _beginthreadex(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, timeTestSub, (<span class="hljs-keyword">void</span>*)i, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);
+ }
+ WaitForMultipleObjects(<span class="hljs-number">8</span>, ghEvents, TRUE, <span class="hljs-number">10000</span>);
+ <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">8</span>; i++) {
+ timeSum += timeResult[i];
+ repeatCounter += threadRepeatCounter[i];
+ fragmentCounter += memFragmentCount[i];
+ }
+}
+</code></pre><br>
+<br>
+<img src="https://habrastorage.org/files/a51/86b/dbd/a5186bdbdbb54baeb50650ef054223f4.jpg">
<div class="clear"></div>
- </cut></cut></cut></cut></cut></cut></div>
+ </cut></cut></cut></cut></cut></div>
<ul class="tags icon_tag">
- <li><a href="https://habrahabr.ru/search/?q=%5B%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%3B%20c%3B%20c%2B%2B%3B%5D&amp;target_type=posts" rel="tag">программирование; c; c++;</a></li>
+ <li><a href="https://habrahabr.ru/search/?q=%5B%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%3B%20c%3B%20c%2B%2B%3B%20asm%3B%5D&amp;target_type=posts" rel="tag">программирование; c; c++; asm;</a></li>
</ul>
<div class="infopanel_wrapper js-user_33766">
<ul class="postinfo-panel postinfo-panel_post" id="infopanel_post_278509">
<li class="postinfo-panel__item">
- <div class="voting-wjt voting-wjt_infopanel js-voting voted">
- <button disabled="disabled" type="button" class="voting-wjt__button voting-wjt__button_plus js-plus" title="Нравится" onclick="return posts_vote(this, 278509, '2', 1)">
+ <div class="voting-wjt voting-wjt_infopanel js-voting ">
+ <button type="button" disabled="" class="voting-wjt__button voting-wjt__button_plus js-plus" title="Срок голосования истек.">
<span>↑</span>
</button>
- <div class="voting-wjt__counter js-mark voting-wjt__counter_positive">
- <span class="voting-wjt__counter-score js-score" title="Всего 26: ↑23 и ↓3">+20</span>
+ <div class="voting-wjt__counter voting-wjt__counter_positive js-mark">
+ <span class="voting-wjt__counter-score js-score" title="Общий рейтинг 28: ↑24 и ↓4">+20</span>
</div>
- <button disabled="disabled" type="button" class="voting-wjt__button voting-wjt__button_minus js-minus" title="Не нравится" onclick="return posts_vote(this, 278509, '2', -1)">
+ <button type="button" disabled="" class="voting-wjt__button voting-wjt__button_minus js-minus" title="Срок голосования истек.">
<span>↓</span>
</button>
</div>
</li>
<li class="postinfo-panel__item">
- <div class="views-count_post" title="Просмотры публикации">9,4k</div>
+ <div class="views-count_post" title="Просмотры публикации">12,1k</div>
</li>
<li class="postinfo-panel__item">
<div class="favorite-wjt favorite">
<button type="button" class="favorite-wjt__button add" title="Добавить в избранное" onclick="return posts_add_to_favorite(this, '2', 278509);">
<span>В избранное</span>
</button>
- <span class="favorite-wjt__counter js-favs_count" title="Количество пользователей, добавивших публикацию в избранное">96</span>
+ <span class="favorite-wjt__counter js-favs_count" title="Количество пользователей, добавивших публикацию в избранное">133</span>
</div>
</li>
<!-- шеринг в соцсети -->
<li class="postinfo-panel__item postinfo-panel__item_socials postinfo-panel__item_socials_right ">
<ul class="post-share">
<li class="post-share__item">
- <a href="https://twitter.com/intent/tweet?text=%D0%A8%D1%83%D1%81%D1%82%D1%80%D1%8B%D0%B9+%D0%BF%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%B1%D0%B5%D0%B7%D0%BE%D0%BF%D0%B0%D1%81%D0%BD%D1%8B%D0%B9+%D0%BC%D0%B5%D0%BD%D0%B5%D0%B4%D0%B6%D0%B5%D1%80+%D0%BA%D1%83%D1%87%D0%B8+%D0%B8+%D0%B3%D0%BE%D0%BB%D1%8B%D0%B9+%D0%A1%D0%B8+http://habr.ru/p/278509/+via+%40habrahabr+%23habr" class="post-share__item-link twitter" title="Опубликовать ссылку в Twitter" target="_blank"></a>
+ <a href="https://twitter.com/intent/tweet?text=%D0%A8%D1%83%D1%81%D1%82%D1%80%D1%8B%D0%B9+%D0%BF%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%B1%D0%B5%D0%B7%D0%BE%D0%BF%D0%B0%D1%81%D0%BD%D1%8B%D0%B9+%D0%BC%D0%B5%D0%BD%D0%B5%D0%B4%D0%B6%D0%B5%D1%80+%D0%BA%D1%83%D1%87%D0%B8%2C+%D0%90%D1%81%D1%8F+%D0%B8+%D0%B3%D0%BE%D0%BB%D1%8B%D0%B5+%D0%A1%D0%B8+http://habr.ru/p/278509/+via+%40habrahabr+%23habr" class="post-share__item-link twitter" title="Опубликовать ссылку в Twitter" target="_blank"></a>
</li>
<li class="post-share__item">
<a href="https://vk.com/share.php?url=https://habrahabr.ru/post/278509/" class="post-share__item-link vkontakte" title="Опубликовать ссылку во ВКонтакте" onclick="window.open(this.href, 'Опубликовать ссылку во Вконтакте', 'width=800,height=300'); return false"></a>
</li>
<li class="post-share__item">
@@ -503,22 +568,22 @@
<span class="user-pic_default user-pic_default_47 user-pic_lilac"></span>
</a>
<div class="author-info__desc js-user_33766">
<div class="author-info__username">
- <a href="/users/Tatuin" class="author-info__name">Татьяна</a>
+ <a href="/users/Tatuin" class="author-info__name">Дмитрий</a>
<a href="/users/Tatuin" class="author-info__nickname">@Tatuin</a>
<div class="karma__widjet voting-wjt voting-wjt_small js-karma " title="Карма пользователя">
<button type="button" class="voting-wjt__button voting-wjt__button_plus js-vote_plus" onclick="return userKarmaVote(this, 33766, 1, 1);" title="Повысить карму">
<span>↑</span>
</button>
- <div class="voting-wjt__counter js-karma-mark voting-wjt__counter_positive " title="12 голосов">
+ <div class="voting-wjt__counter js-karma-mark voting-wjt__counter_positive " title="16 голосов">
<div class="voting-wjt__label">карма</div>
- <div class="voting-wjt__counter-score js-karma_num">10,0</div>
+ <div class="voting-wjt__counter-score js-karma_num">14,0</div>
</div>
<button type="button" class="voting-wjt__button voting-wjt__button_minus js-vote_minus" onclick="return userKarmaVote(this, 33766, 1, -1);" title=" Понизить карму">
<span>↓</span>
</button>
@@ -534,10 +599,13 @@
Написать
</a>
</div>
</div>
+ <div class="author-info__specialization">
+ системостроитель
+ </div>
</div>
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment