En la función $Sequence

Solapas principales

Esta es la traducción del artículo original.

En este artículo vamos a comparar las funciones $Increment y $Sequence.

En primer lugar, una nota para los lectores que nunca han oído hablar de $Increment. $Increment es una función Caché ObjectScript que realiza una operación atómica para incrementar su argumento en 1 y devolver el valor resultante. Solo puede pasar un nodo variable global o local como parámetro a $Increment, no una expresión arbitraria. $Increment se usa mucho al asignar ID secuenciales. En tales casos, el parámetro de $Increment suele ser un nodo global. $Increment garantiza que cada proceso que lo use obtenga una identificación única.

for i=1:1:10000 {
     set Id = $Increment(^Person) ; new Id
     set surname = ##class(%PopulateUtils).LastName() ; random last name
     set name = ##class(%PopulateUtils).FirstName()  ; random first name
     set ^Person(Id) = $ListBuild(surname, name)
}

El problema con $Increment es que si muchos procesos agregan filas en paralelo, estos procesos pueden pasar tiempo esperando su turno para cambiar atómicamente el valor del nodo global que contiene la ID - ^Person en el ejemplo anterior

$Sequence es una nueva función que fue diseñada para manejar este problema. $Sequence está disponible desde Caché 2015.1. Al igual que $Increment, $Sequence incrementa atómicamente el valor de su parámetro. A diferencia de $Increment, $Sequence reservará algunos valores de contador posteriores para el proceso actual y durante la próxima llamada en el mismo proceso simplemente devolverá el siguiente valor del rango reservado. $Sequence calcula automáticamente cuántos valores reservar. Si más a menudo el proceso llama $Sequence, más valores $Sequence reserva:

USER>kill ^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

Cuando $Sequence (^myseq) devolvió 9, los siguientes 8 valores (hasta 16) ya estaban reservados para el proceso actual. Si otro proceso llama a $Sequence, obtendría un valor de 17, no de 10.

$Sequence está diseñado para procesos que incrementan simultáneamente algún nodo global. Dado que los valores de reserva de $Sequence, los ID pueden tener huecos si el proceso no utiliza todos los valores reservados. El uso principal de $Sequence es la generación de ID secuenciales. En comparación con $Sequence, $Increment es una función más genérica.

Comparemos el rendimiento de $Increment y $Sequence:

Class DC.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,!
}

}

El método run ejecuta 10 procesos, cada uno insertando 200.000 registros en el global ^Person. Esperar hasta que finalicen los procesos secundarios. El método run intenta obtener un bloqueo exclusivo en ^P. Cuando los procesos secundarios finalizan su trabajo y liberan el bloqueo compartido en ^P, la ejecución adquirirá un bloqueo exclusivo en ^P y continuará la ejecución. Justo después de esto, registramos el tiempo de la variable del sistema $zhorolog y calculamos cuánto tiempo llevó insertar estos registros. Mi portátil multinúcleo con disco duro lento tardó 40 segundos (como experimento, lo ejecuté varias veces antes, así que esta fue la quinta ejecución):

USER>do ##class(DC.IncSeq.Test).run()
done:39.198488

Es interesante profundizar en estos 40 segundos. Mediante la ejecución de ^%SYS.MONLBL podemos ver que se gastaron 100 segundos en obtener la identificación. 100 segundos / 10 procesos = cada proceso pasó 10 segundos para adquirir una nueva ID, 1.7 segundos para obtener el nombre y apellido, y 28.5 segundos para escribir datos en datos globales.

La primera columna en el informe %SYS.MONLBL a continuación es el número de línea, la segunda es cuántas veces se ejecutó esta línea y la tercera es cuántos segundos tardó en ejecutar esta línea.

 ; ** Source for Method 'filling' **
1            10    .001143    lock +^P:"S"
2            10    .000055    set job = $JOB
3            10    .000118     for i=1:1:200000 {
4       1998499 100.356554         set Id = $Increment(^Person)
5       1993866  10.409804         set surname = ##class(%PopulateUtils).LastName()
6       1990461   6.347832         set name = ##class(%PopulateUtils).FirstName()
7       1999762  285.54603         set ^Person(Id) = $ListBuild(job, surname, name)
8       1999825   3.393706     }
9            10    .000259     lock -^P:"S"
 ; ** End of source for Method 'filling' **
 ;
 ; ** Source for Method 'run' **
1             1    .005503    kill ^Person
2             1    .000002    set z1 = $zhorolog
3             1    .000002    for i=1:1:10 {
4            10    .201327        job ..filling()
5             0          0     }
6             1  43.472692     lock ^P
7             1     .00003     set z2 = $zhorolog - z1
8             1     .00001     lock
9             1    .000053     write "done:",z2,!
 ; ** End of source for Method 'run' **

El tiempo total (43,47 segundos) es 4 segundos más que durante la ejecución anterior debido a la creación de perfiles.

Reemplacemos una cosa en nuestro código de prueba, en el método de filling. Cambiaremos $Increment(^Person) a $Sequence(^Person) y volveremos a ejecutar la prueba:

USER>do ##class(DC.IncSeq.Test).run()
done:5.135189

Este resultado es sorprendente. Ok, $Sequence disminuyó el tiempo para obtener ID, pero ¿a dónde se fueron 28.5 segundos para almacenar datos en global? Verifiquemos ^%SYS.MONLBL:

 ; ** Source for Method 'filling' **
1            10    .001181    lock +^P:"S"
2            10    .000026    set job = $JOB
3            10    .000087     for i=1:1:200000 {
4       1802473   1.996279         set Id = $Sequence(^Person)
5       1784910   4.429576         set surname = ##class(%PopulateUtils).LastName()
6       1853508   3.829051         set name = ##class(%PopulateUtils).FirstName()
7       1838752  32.281624         set ^Person(Id) = $ListBuild(job, surname, name)
8       1951569     1.0243     }
9            10    .000219     lock -^P:"S"
 ; ** End of source for Method 'filling' **
 ;
 ; ** Source for Method 'run' **
1             1    .006514    kill ^Person
2             1    .000002    set z1 = $zhorolog
3             1    .000002    for i=1:1:10 {
4            10    .385055        job ..filling()
5             0          0     }
6             1   6.558119     lock ^P
7             1    .000011     set z2 = $zhorolog - z1
8             1    .000008     lock
9             1    .000025     write "done:",z2,!
 ; ** End of source for Method 'run' **

Ahora, cada proceso gasta 0,2 segundos en lugar de 10 segundos para la adquisición de ID. Lo que no está claro es por qué el almacenamiento de datos lleva solo 3,23 segundos por proceso. La razón es que los nodos globales se almacenan en bloques de datos, y generalmente cada bloque tiene un tamaño de 8192 bytes. Antes de cambiar el valor del nodo global (como set ^Person(Id) = ...), el proceso bloquea todo el bloque. Si varios procesos intentan cambiar datos dentro del mismo bloque al mismo tiempo, solo un proceso podrá cambiar el bloque y otros tendrán que esperar a que termine.

Echemos un vistazo a global creado usando $Increment para generar nuevas ID. Los registros secuenciales casi nunca tendrían la misma ID de proceso (recuerde, almacenamos la ID de proceso como el primer elemento de la lista de datos):

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")

Los procesos concurrentes intentaban escribir datos en el mismo bloque y pasaban más tiempo esperando que en realidad cambiando los datos. Usando $Sequence, las ID se generan en fragmentos, por lo que los diferentes procesos probablemente usarían diferentes bloques:

1:     ^Person(100000)    =    $lb("12963","Yezek","Amanda")
// 351 records with process number 12963
353:     ^Person(100352)    =    $lb("12963","Young","Lola")
354:     ^Person(100353)    =    $lb("12967","Roentgen","Barb")

Si esta muestra se parece a algo que está haciendo en sus proyectos, considere usar $Sequence en lugar de $Increment. Por supuesto, consulte en la documentación antes de reemplazar cada aparición de $Increment con $Sequence.

Y claro, no creas en las pruebas proporcionadas aquí: verifica esto tú mismo.

A partir de Caché 2015.2, puede configurar tablas para usar $Sequence en lugar de $Increment. No es una función del sistema $system.Sequence.SetDDLUseSequence para eso, y la misma opción está disponible en Configuración de SQL en el Portal de administración.

Además, hay un nuevo parámetro de almacenamiento en la definición de clase: IDFunction, que está configurado en "incremento" de forma predeterminada, eso significa que $Increment se usa para la generación de Id. Puede cambiarlo a "secuencia" (Inspector> Almacenamiento> Predeterminado> Función ID).

Bonus

Otra prueba rápida que realicé en mi computadora portátil: es una configuración ECP pequeña con el servidor DB ubicado en el sistema operativo host y el servidor de aplicaciones en la máquina virtual host en la misma computadora portátil. Mapeé ^Person a la base de datos remota. Es una prueba básica, por lo que no quiero hacer generalizaciones basadas en ella. Hay cosas a considerar cuando se usa $Increment y ECP. Dicho esto, aquí están los resultados:

With $Increment:

USER>do ##class(DC.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' **
 ;
 ; ** Source for Method 'run' **
1             1    .000045    kill ^Person
2             1    .000001    set z1 = $zhorolog
3             1    .000007    for i=1:1:10 {
4            10    .059868        job ..filling()
5             0          0    }
6             1 170.342459    lock ^P
7             1    .000005    set z2 = $zhorolog - z1
8             1    .000013    lock
9             1    .000018    write "done:",z2,!
 ; ** End of source for Method 'run' **

$Sequence:

USER>do ##class(DC.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' **
 ;
 ; ** Source for Method 'run' **
1             1    .000046    kill ^Person
2             1    .000002    set z1 = $zhorolog
3             1    .000004    for i=1:1:10 {
4            10    .037271        job ..filling()
5             0          0    }
6             1  14.620781    lock ^P
7             1    .000005    set z2 = $zhorolog - z1
8             1    .000013    lock
9             1    .000016    write "done:",z2,!
 ; ** End of source for Method 'run' **