La respuesta de broofa es bastante hábil, de hecho, impresionantemente inteligente, realmente ... cumple con rfc4122, algo legible y compacto. ¡Increíble!
Pero si está viendo esa expresión regular, esas muchas replace()
devoluciones de llamada, llamadas de función toString()
y Math.random()
llamadas de función (donde solo usa 4 bits del resultado y desperdicia el resto), puede comenzar a preguntarse sobre el rendimiento. De hecho, joelpt incluso decidió tirar RFC para obtener una velocidad GUID genérica generateQuickGUID
.
Pero, ¿podemos obtener velocidad y cumplimiento de RFC? ¡Yo digo si! ¿Podemos mantener la legibilidad? Bueno ... En realidad no, pero es fácil si sigues.
Pero primero, mis resultados, en comparación con broofa, guid
(la respuesta aceptada) y el no compatible con rfc generateQuickGuid
:
Desktop Android
broofa: 1617ms 12869ms
e1: 636ms 5778ms
e2: 606ms 4754ms
e3: 364ms 3003ms
e4: 329ms 2015ms
e5: 147ms 1156ms
e6: 146ms 1035ms
e7: 105ms 726ms
guid: 962ms 10762ms
generateQuickGuid: 292ms 2961ms
- Note: 500k iterations, results will vary by browser/cpu.
Entonces, en mi sexta iteración de optimizaciones, superé la respuesta más popular en más de 12X , la respuesta aceptada en más de 9X y la respuesta rápida no compatible en 2-3X . Y sigo siendo compatible con rfc4122.
¿Interesado en cómo? Puse la fuente completa en http://jsfiddle.net/jcward/7hyaC/3/ y en http://jsperf.com/uuid-generator-opt/4
Para una explicación, comencemos con el código de broofa:
function broofa() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}
console.log(broofa())
Por lo tanto, se reemplaza x
con cualquier dígito hexadecimal aleatorio, y
con datos aleatorios (excepto forzar los 2 bits superiores 10
según la especificación RFC), y la expresión regular no coincide con los caracteres -
o 4
, por lo que no tiene que lidiar con ellos. Muy, muy hábil.
Lo primero que debe saber es que las llamadas a funciones son caras, al igual que las expresiones regulares (aunque solo usa 1, tiene 32 devoluciones de llamada, una para cada coincidencia, y en cada una de las 32 devoluciones de llamada llama a Math.random () y v. toString (16)).
El primer paso hacia el rendimiento es eliminar el RegEx y sus funciones de devolución de llamada y utilizar un bucle simple. Esto significa que tenemos que lidiar con los caracteres -
y 4
mientras que broofa no. Además, tenga en cuenta que podemos usar la indexación de String Array para mantener su elegante arquitectura de plantilla de String:
function e1() {
var u='',i=0;
while(i++<36) {
var c='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'[i-1],r=Math.random()*16|0,v=c=='x'?r:(r&0x3|0x8);
u+=(c=='-'||c=='4')?c:v.toString(16)
}
return u;
}
console.log(e1())
Básicamente, la misma lógica interna, excepto que verificamos -
o 4
, y el uso de un ciclo while (en lugar de replace()
devoluciones de llamada) nos da una mejora casi 3X.
El siguiente paso es pequeño en el escritorio, pero hace una diferencia decente en el móvil. Hagamos menos llamadas Math.random () y utilicemos todos esos bits aleatorios en lugar de tirar el 87% de ellos con un búfer aleatorio que se desplaza en cada iteración. Muevamos también esa definición de plantilla fuera del ciclo, en caso de que ayude:
function e2() {
var u='',m='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx',i=0,rb=Math.random()*0xffffffff|0;
while(i++<36) {
var c=m[i-1],r=rb&0xf,v=c=='x'?r:(r&0x3|0x8);
u+=(c=='-'||c=='4')?c:v.toString(16);rb=i%8==0?Math.random()*0xffffffff|0:rb>>4
}
return u
}
console.log(e2())
Esto nos ahorra un 10-30% dependiendo de la plataforma. No está mal. Pero el siguiente gran paso es deshacerse de las llamadas a la función toString con un clásico de optimización: la tabla de búsqueda. Una simple tabla de búsqueda de 16 elementos realizará el trabajo de toString (16) en mucho menos tiempo:
function e3() {
var h='0123456789abcdef';
var k='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
/* same as e4() below */
}
function e4() {
var h=['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'];
var k=['x','x','x','x','x','x','x','x','-','x','x','x','x','-','4','x','x','x','-','y','x','x','x','-','x','x','x','x','x','x','x','x','x','x','x','x'];
var u='',i=0,rb=Math.random()*0xffffffff|0;
while(i++<36) {
var c=k[i-1],r=rb&0xf,v=c=='x'?r:(r&0x3|0x8);
u+=(c=='-'||c=='4')?c:h[v];rb=i%8==0?Math.random()*0xffffffff|0:rb>>4
}
return u
}
console.log(e4())
La próxima optimización es otro clásico. Dado que solo estamos manejando 4 bits de salida en cada iteración de bucle, reduzcamos a la mitad el número de bucles y procesemos 8 bits en cada iteración. Esto es complicado ya que todavía tenemos que manejar las posiciones de bits compatibles con RFC, pero no es demasiado difícil. Luego tenemos que hacer una tabla de búsqueda más grande (16x16 o 256) para almacenar 0x00 - 0xff, y la compilamos solo una vez, fuera de la función e5 ().
var lut = []; for (var i=0; i<256; i++) { lut[i] = (i<16?'0':'')+(i).toString(16); }
function e5() {
var k=['x','x','x','x','-','x','x','-','4','x','-','y','x','-','x','x','x','x','x','x'];
var u='',i=0,rb=Math.random()*0xffffffff|0;
while(i++<20) {
var c=k[i-1],r=rb&0xff,v=c=='x'?r:(c=='y'?(r&0x3f|0x80):(r&0xf|0x40));
u+=(c=='-')?c:lut[v];rb=i%4==0?Math.random()*0xffffffff|0:rb>>8
}
return u
}
console.log(e5())
Probé un e6 () que procesa 16 bits a la vez, todavía usando la LUT de 256 elementos, y mostró los rendimientos decrecientes de la optimización. Aunque tuvo menos iteraciones, la lógica interna se complicó por el mayor procesamiento, y realizó lo mismo en el escritorio, y solo ~ 10% más rápido en dispositivos móviles.
La técnica de optimización final para aplicar: desenrollar el bucle. Como estamos haciendo un bucle un número fijo de veces, técnicamente podemos escribir todo esto a mano. Intenté esto una vez con una sola variable aleatoria r que seguí reasignando, y el rendimiento se derrumbó. Pero con cuatro variables asignadas datos aleatorios por adelantado, luego usando la tabla de búsqueda y aplicando los bits RFC adecuados, esta versión los fuma a todos:
var lut = []; for (var i=0; i<256; i++) { lut[i] = (i<16?'0':'')+(i).toString(16); }
function e7()
{
var d0 = Math.random()*0xffffffff|0;
var d1 = Math.random()*0xffffffff|0;
var d2 = Math.random()*0xffffffff|0;
var d3 = Math.random()*0xffffffff|0;
return lut[d0&0xff]+lut[d0>>8&0xff]+lut[d0>>16&0xff]+lut[d0>>24&0xff]+'-'+
lut[d1&0xff]+lut[d1>>8&0xff]+'-'+lut[d1>>16&0x0f|0x40]+lut[d1>>24&0xff]+'-'+
lut[d2&0x3f|0x80]+lut[d2>>8&0xff]+'-'+lut[d2>>16&0xff]+lut[d2>>24&0xff]+
lut[d3&0xff]+lut[d3>>8&0xff]+lut[d3>>16&0xff]+lut[d3>>24&0xff];
}
console.log(e7())
Modualizado: http://jcward.com/UUID.js -UUID.generate()
Lo curioso es que generar 16 bytes de datos aleatorios es la parte fácil. Todo el truco es expresarlo en formato de cadena con cumplimiento de RFC, y se logra con 16 bytes de datos aleatorios, un bucle desenrollado y una tabla de búsqueda.
Espero que mi lógica sea correcta: es muy fácil cometer un error en este tipo de trabajo tedioso. Pero los resultados me parecen buenos. ¡Espero que hayas disfrutado este loco viaje a través de la optimización del código!
Tenga en cuenta: mi objetivo principal era mostrar y enseñar posibles estrategias de optimización. Otras respuestas cubren temas importantes como colisiones y números verdaderamente aleatorios, que son importantes para generar buenos UUID.