Небольшое отступление для читателей, которым и менять-то нечего, и слово $Increment они видят впервые. $Increment — встроенная функция в Cache ObjectScript — атомарная операция. Аргументом функции $Increment может быть только переменная — не выражение. $Increment неявно блокирует переменную, увеличивает её значение на 1, разблокирует переменную, возвращает новое значение. $Increment широко используется, когда нужно назначать числовой идентификатор типа счётчик новым объектам или записям, в таких случаях аргументом функции является имя глобала. Выглядит это примерно так:
for i=1:1:10000 {
set id = $Increment(^Person)
set surname = ##class(%PopulateUtils).LastName() ; случайная фамилия
set name = ##class(%PopulateUtils).FirstName() ; случайное имя
set ^Person(id) = $ListBuild(surname, name)
}
Что же такое $Sequence? Эта функция появилась в версии 2015.1, как и $Increment она выполняет атомарную операцию и возвращает увеличенное на 1 значение своего аргумента.
USER>set $Sequence(^myseq)=""
USER>for i=1:1:15 {write "increment:",$Seq(^myseq)," allocated:",^myseq,! }
increment:1 allocated:1
increment:2 allocated:2
increment:3 allocated:4
increment:4 allocated:4
increment:5 allocated:8
increment:6 allocated:8
increment:7 allocated:8
increment:8 allocated:8
increment:9 allocated:16
increment:10 allocated:16
increment:11 allocated:16
increment:12 allocated:16
increment:13 allocated:16
increment:14 allocated:16
increment:15 allocated:16
Видите, когда $Sequence(^myseq) вернула 9, следующие 8 значений (до 16) уже были закешированы для текущего процесса. Параллельный процесс, обратившийся к $Sequence(^myseq), получил бы значение 17.
$Sequence предназначена для использования в процессах, которые параллельно увеличивают одно и то же глобальное значение. Поскольку $Sequence кеширует значения порциями, то в порядке идентификаторов могут быть пропуски, если процесс использовал не все выделенные ему значения. Собственно, основное предназначение функции $Sequence — генерация уникальных значений счётчика. $Increment в этом смысле, несколько более общая функция.
Чтобы сравнить $Increment и $Sequence, запустим небольшой пример:
Class Habr.IncSeq.Test
{
ClassMethod filling()
{
lock +^P:"S"
set job = $job
for i=1:1:200000 {
set id = $Increment(^Person)
set surname = ##class(%PopulateUtils).LastName()
set name = ##class(%PopulateUtils).FirstName()
set ^Person(id) = $ListBuild(job, surname, name)
}
lock -^P:"S"
}
ClassMethod run()
{
kill ^Person
set z1 = $zhorolog
for i=1:1:10 {
job ..filling()
}
lock ^P
set z2 = $zhorolog - z1
lock
write "done:",z2,!
}
}
Метод run запускает десять процессов, каждый из которых вставляет 200000 записей в глобал ^Person. Блокировка на глобал ^P нужна только для того, чтобы родительский процесс дождался окончания работы дочерних процессов. Поэтому он пытается получить экслюзивную блокировку на глобал ^P, но получит он её только тогда, когда все дочерние процессы доделают свою работу и снимут разделяемую блокировку; сразу после этого мы считываем отсечку времени ($zhorolog) ещё раз, снимаем полученную блокировку на ^P и смотрим, сколько секунд заняла вставка записей. На моём четырёхъядерном ноутбуке, выполнение метода run заняло 21 секунду (для зануд скажу, что это был уже пятый запуск этого же метода):
USER>do ##class(Habr.IncSeq.Test).run()
done:21.40948
Интересно узнать, на что ушла эта 21 секунда. Запустив ^%SYS.MONLBL (про который, кстати, была статья на хабре), видим cледующую картину:
; ** Source for Method 'filling' **
1 10 .000433 lock +^P:"S"
2 10 .000013 set job = $job
3 10 .000038 for i=1:1:200000 {
4 1999991 13.222959 set id = $Increment(^Person)
5 1997246 7.029486 set surname = ##class(%PopulateUtils).LastName()
6 1995420 4.766967 set name = ##class(%PopulateUtils).FirstName()
7 1999680 208.226093 set ^Person(id) = $ListBuild(job, surname, name)
8 1999790 1.69106 }
9 10 .000205 lock -^P:"S"
; ** End of source for Method 'filling' **
;
; ** Source for Method 'run' **
1 1 .01005 kill ^Person
2 1 .000003 set z1 = $zhorolog
3 1 .000004 for i=1:1:10 {
4 10 .056381 job ..filling()
5 0 0 }
6 1 26.244814 lock ^P
7 1 .000003 set z2 = $zhorolog - z1
8 1 .000006 lock
9 1 .000009 write "done:",z2,!
; ** End of source for Method 'run' **
Первый столбец в отчёте ^%SYS.MONLBL — номер строки в методе, второй — количество выполнений этой строки, третий — сколько секунд выполнялась эта строка.
В общей сложности 13.2 секунды было потрачено на получение Id. Разделив 13.2 на количество процессов, получим, что каждый из них потратил 1.32 секунды на получение нового Id, 1.1 секунды на вычисление имени и фамилии, и 20.8 секунд на запись данных в глобал. Общее время (26.24) получилось на 5 секунд больше из-за профилировщика.
Давайте заменим в нашем тесте (а именно в методе filling()) $Increment(^Person) на $Sequence(^Person) и запустим тест ещё раз:
USER>do ##class(Habr.IncSeq.Test).run()
done:3.324123
Результат удивительный. Пусть $Sequence уменьшила время получения Id, но куда делись 20.8 секунд на запись данных? Смотрим результаты ^%SYS.MONLBL:
; ** Source for Method 'filling' **
1 10 .000523 lock +^P:"S"
2 10 .000017 set job = $job
3 10 .000048 for i=1:1:200000 {
4 1911382 1.69533 set id = $Sequence(^Person)
5 1753050 3.783609 set surname = ##class(%PopulateUtils).LastName()
6 1830006 3.407867 set name = ##class(%PopulateUtils).FirstName()
7 1827874 21.544164 set ^Person(id) = $ListBuild(job, surname, name)
8 1879819 .843424 }
9 10 .00023 lock -^P:"S"
; ** End of source for Method 'filling' **
;
; ** Source for Method 'run' **
1 1 .010926 kill ^Person
2 1 .000004 set z1 = $zhorolog
3 1 .000004 for i=1:1:10 {
4 10 .049543 job ..filling()
5 0 0 }
6 1 5.090719 lock ^P
7 1 .000003 set z2 = $zhorolog - z1
8 1 .000007 lock
9 1 .00001 write "done:",z2,!
; ** End of source for Method 'run' **
На получение Id каждый процесс теперь тратит 0.17 секунды вместо 1.32. Но почему на запись в базу тратится 2.15 секунд на процесс? Как такое может быть? Дело в том, что глобалы хранятся в блоках по (обычно) 8 килобайт каждый. Каждый процесс перед изменением глобала (таким как set ^Person(id) = …) получает внутреннюю блокировку на блок. Если несколько процессов пытаются изменить один и тот же блок — один процесс ждёт, пока другой освободит блок. Если таких процессов десять, то девять ждут одного. Если посмотреть на глобал ^Person, созданный с $increment, то можно увидеть, что почти никогда две соседние записи не созданы одним процессом:
1: ^Person(100000) = $lb("12950","Kelvin","Lydia")
2: ^Person(100001) = $lb("12943","Umansky","Agnes")
3: ^Person(100002) = $lb("12945","Frost","Natasha")
4: ^Person(100003) = $lb("12942","Loveluck","Terry")
5: ^Person(100004) = $lb("12951","Russell","Debra")
6: ^Person(100005) = $lb("12947","Wells","Chad")
7: ^Person(100006) = $lb("12946","Geoffrion","Susan")
8: ^Person(100007) = $lb("12945","Lennon","Roberta")
9: ^Person(100008) = $lb("12944","Beatty","Mark")
10: ^Person(100009) = $lb("12946","Kovalev","Nataliya")
11: ^Person(100010) = $lb("12947","Klingman","Olga")
12: ^Person(100011) = $lb("12942","Schultz","Alice")
13: ^Person(100012) = $lb("12949","Young","Filomena")
14: ^Person(100013) = $lb("12947","Klausner","James")
15: ^Person(100014) = $lb("12945","Ximines","Christine")
16: ^Person(100015) = $lb("12948","Quine","Mary")
17: ^Person(100016) = $lb("12948","Rogers","Sally")
18: ^Person(100017) = $lb("12950","Ueckert","Thelma")
19: ^Person(100018) = $lb("12944","Xander","Kim")
20: ^Person(100019) = $lb("12948","Ubertini","Juanita")
Параллельные процессы пытались пробится к одному и тому же блоку, и дольше ждали своей очереди на запись в блок, чем реально меняли данные. В случае с $Sequence, Id выдаются большими кусками, разнося разные процессы по разные блокам:
1: ^Person(100000) = $lb("12963","Yezek","Amanda")
// 351 запись с номером процесса 12963
353: ^Person(100352) = $lb("12963","Young","Lola")
354: ^Person(100353) = $lb("12967","Roentgen","Barb")
«Все это здорово», скажет читатель, но ведь при объектном и SQL-доступе Cache за нас использует $Increment для генерации новых Id. Как использовать $Sequence? Начиная с версии 2015.1 параметр хранения IDFunction определяет функцию, генерирующую Id. По умолчанию он равен «increment», но вы можете изменить его на «sequence» (В инспекторе Студии выберите Storage > Default > IDFunction)
В заключение:
Не верьте ничему, что здесь написано. Я специально не пишу характеристики компьютера и настройки экземпляра Cache, на котором я запускал этот тест — лучше запустите его сами.
Бонус
В качестве ещё одного теста я собрал небольшую ECP конфигурацию с сервером баз данных на ноутбуке и сервером приложений на виртуальной машине внутри этого ноутбука. Настроил отображение глобала ^Person в удалённую (remote, а не removed) базу. Ни о какой репрезентативности этого теста речи быть не может. $Increment c ECP нужно использовать аккуратно. Тем не менее, вот результаты:
$Increment
USER>do ##class(Habr.IncSeq.Test).run()
done:163.781288
^%SYS.MONLBL:
; ** Source for Method 'filling' **
1 10 .000503 lock +^P:"S"
2 10 .000016 set job = $job
3 10 .000044 for i=1:1:200000 {
4 1843745 1546.57015 set id = $Increment(^Person)
5 1880231 6.818051 set surname = ##class(%PopulateUtils).LastName()
6 1944594 3.520858 set name = ##class(%PopulateUtils).FirstName()
7 1816896 16.576452 set ^Person(id) = $ListBuild(job, surname, name)
8 1933736 .895912 }
9 10 .000279 lock -^P:"S"
; ** End of source for Method 'filling' **
$Sequence
USER>do ##class(Habr.IncSeq.Test).run()
done:13.826716
^%SYS.MONLBL:
; ** Source for Method 'filling' **
1 10 .000434 lock +^P:"S"
2 10 .000014 set job = $job
3 10 .000033 for i=1:1:200000 {
4 1838247 98.491738 set id = $Sequence(^Person)
5 1712000 3.979588 set surname = ##class(%PopulateUtils).LastName()
6 1809643 3.522974 set name = ##class(%PopulateUtils).FirstName()
7 1787612 16.157567 set ^Person(id) = $ListBuild(job, surname, name)
8 1862728 .825769 }
9 10 .000255 lock -^P:"S"
; ** End of source for Method 'filling' **
У функции $Sequence есть некоторые ограничения — перед использованием ознакомьтесь с документацией.
Спасибо за внимание!
Комментарии (24)
jxcoder
31.07.2015 15:03+2Вопрос к Cache разработчикам: зачем sequence? Почему бы не улучшить increment?
morisson
31.07.2015 15:05+1Я не разработчик, но постараюсь ответить. Например потому, что $sequence только для глобалов.
jxcoder
31.07.2015 15:08+2Ну, это я прочитал, но вопрос «почему» остался. Почему всё таки не научить метод работать с глобалами? В Cache есть универсальные функции, которые могут работать с различными типами аргументов и всё ок. А чего бы тут так не сделать? м?
morisson
31.07.2015 15:14+1Опять же вопрос к разработчикам, вполне резонный впрочем, возможно в будущем это появится. Предпололожу, что можно написать такой хитрый макрос, какой-нибудь SmartInc, который будет разбираться по аргументу, глобал там или нет, и делать правильный выбор. Судя по тому, что вы хорошо пишете макросы, может быть и вы сможете.
morisson
31.07.2015 15:16+1не научить метод работать с глобалами?
С глобалами-то он работает прекрасно. А для работы локальными переменными $sequence не предзначначен, пока.
adaptun Автор
31.07.2015 15:15+1Я тоже не разработчик. И тоже постараюсь ответить.
У функции $Increment есть определённая спецификация (контракт), которую она выполняет. Написано много когда, который на эту спецификацию расчитывает. $Sequence работает быстрее, за счёт нарушения этой спецификация. Даже не так — это вообще другая функция, с другой спецификацией (кеширование, шаг только в 1 и только вверх и т.д), которая может быть использована для той же цели, что и $Increment.
$Increment и так улучшается, но без нарушения совместимости.
DAiMor
31.07.2015 21:11+5Я отвечу, так как мне важны различия между этими двумя функциями.
самое главное у $increment это гарантия последовательности нумерации, в некоторых случаях это может быть важно, если это быстрорастущая система, с мбольшим количеством добавлений в минуту/секунду (нужное подчеркнуть), и которое будет вестись неоднородно различными процессами, это неизбежно приведет к пустотам в нумерации.
Далее в совокупности с другой возможностью Cache bitmap индексами, которые для тех кто не в курсе состоят из массива битовых строк разбитых на 64000 бита, каждый бит которого должен указывать на ID объекта. Так вот представим, что объектов у нас в системе миллиарды (вполне реально) делим эти миллиарды на чанки по 64000, и рост их по несколько в день. Далее, да мы помним о том что IS озаботилась так же об оптимизации хранении бит, и то что в таком чанке несколько установленных бит в 0 будет занимать совсем немного, но неоднородность работы $sequence может свести эту оптимизацию на нет, и приведет к тому, что такой индекс может занимать достаточно много места, обычно один блок это 8192 байта, но битовая строка из 64000 бит это максимум примерно 8005 байт, что вместе со служебной информацией самого блока, не влезает в один блок, и такие данные уже разбиваются, на 2 (big blocks) итого 16384 байт на один чанк, Здесь так же упоминается и работа в ECP конфигурации, в таком случае big блоки не кешируются, приходится сразу переходить на 16к блоки.
как писал раньше, имеем 1 миллиард объектов, это получится 1000000000/64000*16384/1024/1024 > 240 Мб, хм, может где ошибся, но вроде все верно. А с такими массивами еще нужно оперировать выполняя разные логические операции.
Вот в такой ситуации $sequence противопоказан, так как давая прирост в одном месте сильно мешает в другом.
Но в целом можно найти применение данной функции в других местах.morisson
02.08.2015 12:01+2Хороший комментарий. Одно соображение — если битмап-индексы делать не на той же OLTP базе, а на хранилище данных, вынесенном на другой сервер, то, похоже, $sec в этом случае можно использовать в обоих местах: в OLTP для быстрорастущих записей (и не использовать битмап индексы вообще) и в OLAP для нумерации фактов (например фактов DeepSee), которые создавать заново подряд, создавать все необходимые битмап и битслайс индексы и использовать их для таблицы фактов эффективно.
adaptun Автор
03.08.2015 12:04+1Нужно смотреть в каждом конкретном случае, какой будет процент дырок. Это зависит от того, как данные загружаются.
В примере из статьи, после выполнения загрузки, значение ^Person = 2007040.
То есть, узлы 2'000'001 — 2'007'040 значений не имеют, и следующий узел будет вставлен в 2'007'041
7'040 / 2'000'000 = 0,00352 = 0.352%
С другой стороны, если сохранение каждого объекта влечёт обновление многих индексов, то, возможно, $Increment не узкое место, и заменять его смысла нет.
AlexeyMaslov
31.07.2015 15:49+1Спасибо за статью, но мотивы разработчиков для введения новой функции и для меня остались неясны…
Соглашусь с jxcoder, вполне можно было бы добавить в $Increment кэширование, аналогичное $Sequence, не нарушая её спецификации: нигде ведь не сказано, что при параллельно работающих N процессах, обновляющих один глобал (как в вашем примере), значения id должны распределяться приблизительно равномерно. С приращениями > 1 этот кэш также отлично бы справился. «Дырки» в глобале, которые неизбежно будут получаться, тоже не проблема: они могут быть и сейчас при аварийном завершении процессов/откате транзакций. То, что $I() работает и с глобальными, и с локальными переменными, а $Sequence() — нет, и вовсе «гнилая отговорка»: $I() и сейчас по-разному работает в обоих случаях, очевидно, что каждый случай обслуживает отдельная ветка кода.adaptun Автор
31.07.2015 16:00+2Пожалуйста, Алексей. Всё равно не очень понятно.
Пусть в программе написано
set id = $Increment(^a)
За счёт чего здесь может происходить кеширование? Ведь функция $Increment должна увеличить значение ^a на 1.
Работать с локальными переменными функции $Sequence, по-моему мнению нет никакой необходимости (как и функции $Increment), ведь внутри одного процесса конкурентного доступа нет, и set a = $Increment(a) это то же самое, что и set a = $Get(a) + 1.
Разброс Id можно, конечно, сделать и без $Sequence. В данном случае это побочный эффект.
AlexeyMaslov
31.07.2015 16:35+1Ведь функция $Increment должна увеличить значение ^a на 1
Равенство id == ^a гарантируется только в момент присваивания, не так ли? Нигде не обещается, что уже в следующую секунду не налетят «100500 процессов» и не увеличат ^a ещё на 100500.
Про локальные переменные упомянул только потому, что вы указали на неспособность работать с ними $seq как на важное отличие от $i. Нет необходимости — да, согласен, но возможность есть, как и во многих других $-функциях, скорее всего, для сохранения полноты языка.
Выделять разброс Id в какое-то отдельное аномальное явление не стоит — это жизнь, т.к. любой программист, использующий
tstart set id=$i(^a),^a(id)=«что-нибудь» tcommit
не должен полагаться на отсутствие дыр в индексации ^a. Что принципиально изменится, если этих дыр станет немного больше?
Вы и сами в заголовке статьи пишете «а вы уже поменяли?». Что понимается под заменой? Могу ли взять и поменять по контексту /$i(^/ на /$seq(^/, если знаю, что никогда не пользуюсь двухаргументной формой $i()?
Если да, то спецификации функций совместимы.
Если нет, объясните, почему.adaptun Автор
31.07.2015 17:10+3Ведь функция $Increment должна увеличить значение ^a на 1
Равенство id == ^a гарантируется только в момент присваивания, не так ли? Нигде не обещается, что уже в следующую секунду не налетят «100500 процессов» и не увеличат ^a ещё на 100500.
Я имел в виду, что спецификация $Increment(^a) — это увеличить значение глобала ^a ровно на 1. У $Sequence такого ограничения нет.
В общем случае, одноаргументный $Increment нельзя заменить на $Sequence? Радикальный пример:
for i=1:1:20 { set a = $Increment(^a) write "^a=",^a,! if i#10=0 { kill ^a } }
В некоторых популярных случаях одноаргументный $Increment можно заменить на $Sequence. В общем случае — нет.
Если вы используете $Increment исключительно для генерации Id, как в примере статьи, то я не знаю причин, чтобы не заменить его на $Sequence (что, конечно же, не значит, что их нет).AlexeyMaslov
31.07.2015 17:56Спасибо за разъяснение.
Если вы используете $Increment исключительно для генерации Id, как в примере статьи, то я не знаю причин, чтобы не заменить его на $Sequence (что, конечно же, не значит, что их нет).
Сухой остаток: InterSystems вводит в язык новую фичу и предлагает на неё переходить, не формулируя чётко критерии, когда это возможно.
Немного успокаивает, что, на вскидку, пострадает лишь искусственный код типа вашего последнего примера (не всё ли равно, что было в вершине глобала, если его всё равно кильнули?), но где гарантия, что нет более тонких слачаев? Именно это и настораживает. Одно дело — писание чего-то нового, другое дело — поддержка и развитие сотен тысяч строк существующего кода, ответственность перед коллегами и клиентами, и т.д.adaptun Автор
31.07.2015 18:25+3Я могу в начале статьи вставить дисклаймер, что статья частная позиция автора и ни в коей мере не позиция фирмы и так далее и тому подобное. Я хотел рассказать о новой интересной функции. Это вполне удалось. Что с ней делать или не делать — решать вам, как разработчику.
«не формулируя чётко критерии, когда это возможно»
Достаточно чётко, что делает функция $Sequence описано в документации.
Можно ли в ваших сценариях использования менять $Increment на $Sequence решать вам. Правильно, что вас настораживает. Нельзя автоматом менять А на Б в сотне тысяч строк кода.
adaptun Автор
31.07.2015 17:18+3Я последовал своему совету и почитал-таки документацию про $Sequence. Можно эту функцию использовать с локальными переменным.
jxcoder
А почему бы в статье не указать какие именно ограничения? Про численное переполнение?
adaptun Автор
Чтоб документацию читали :-)
Численное переполнение, если начать с 1 никому не грозит.
jxcoder
А что сложного-то взять да указать в статье и написать остальное читайте в документации? Почему я должен лезть и открывать документацию. Почему мне предлагают перейти с одного на другое, но не говорят что именно плохого может случиться?
Да дело не в копировании документации. А просто в отношении к людям (к разработчикам). Это отношение очень отталкивает.
adaptun Автор
Ну извините, если обидел. Не хотел ничего такого.
Вы, конечно, никуда лезть не должны.
Я постарался взять минимум из справки по $Sequence — зачем делать перевод на русский язык (почти наверняка корявый), того что просто, понятно, с примерами и не слишком длинно написано по-английски.
doublefint
«Как страшно жить!» — вы вообще с этой планеты?
jxcoder
А что смущает?
doublefint
Ну, планета Земля (Грязь)…
Вас ненавязчиво подталкивают ознакомится с документацией.
И вдруг такая феерия эмоций — «Дело в отношении к людям!!!», «Это… очень отталкивает»
И это еще лето, солнце. Что же зимой то будет? Волнуюсь я за вас