...

пятница, 31 июля 2015 г.

А вы уже поменяли $Increment на $Sequence?

Если бы это была запись для твиттера, то она была бы следующей: «Используйте $Sequence вместо $Increment для генерации Id». Но тут Хабр, поэтому придётся развернуть мысль – добро пожаловать под кат.


Небольшое отступление для читателей, которым и менять-то нечего, и слово $Increment они видят впервые. $Increment — встроенная функция в Caché 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(surnamename)
 
}


Что же такое $Sequence? Эта функция появилась в версии 2015.1, как и $Increment она выполняет атомарную операцию и возвращает увеличенное на 1 значение своего аргумента. В отличие от $Increment, аргументом $Sequence может быть только глобал (не локальная переменная). При первом обращении процесса к $Sequence от определённого глобала, $Sequence кеширует набор возвращаемых значений и при последующих обращениях возвращает значения из кеша. Значение глобала увеличивается на количество закешированных значений. Когда значения в кеше заканчиваются, $Sequence кеширует новый набор, опять увеличивая значение глобала. $Sequence автоматически определяет количество значений, которые нужно закешировать. Чем чаще процесс обращается к $Sequence, тем больше значений будет закешировано:
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(jobsurnamename)
     
}
     
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-доступе Caché за нас использует $Increment для генерации новых Id. Как использовать $Sequence? Начиная с версии 2015.1 параметр хранения IDFunction определяет функцию, генерирующую Id. По умолчанию он равен «increment», но вы можете изменить его на «sequence» (В инспекторе Студии выберите Storage > Default > IDFunction)

В заключение:

Не верьте ничему, что здесь написано. Я специально не пишу характеристики компьютера и настройки экземпляра Caché, на котором я запускал этот тест — лучше запустите его сами.


Бонус


В качестве ещё одного теста я собрал небольшую 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 есть некоторые ограничения — перед использованием ознакомьтесь с документацией.

Спасибо за внимание!

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Комментариев нет:

Отправить комментарий