a resposta de broofa é bastante lisa, de fato - impressionantemente inteligente, realmente ... compatível com rfc4122, um tanto legível e compacta. Impressionante!
Mas se você está olhando para essa expressão regular, aqueles muitos replace()
retornos de chamada, toString()
's e Math.random()
chamadas de função (onde ele está apenas usando 4 bits do resultado e desperdiçando o resto), você pode começar a se perguntar sobre o desempenho. De fato, o joelpt até decidiu jogar fora o RFC para obter velocidade GUID genérica generateQuickGUID
.
Mas, podemos obter velocidade e conformidade com RFC? Eu digo sim! Podemos manter a legibilidade? Bem ... Na verdade não, mas é fácil se você seguir em frente.
Mas primeiro, meus resultados, comparados com o broofa guid
(a resposta aceita) e o que não é compatível com 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.
Então, na minha sexta iteração de otimizações, superei a resposta mais popular em mais de 12X , a resposta aceita em mais de 9 vezes e a resposta rápida não-conformidade em 2-3 vezes . E ainda sou compatível com rfc4122.
Interessado em como? Coloquei a fonte completa em http://jsfiddle.net/jcward/7hyaC/3/ e em http://jsperf.com/uuid-generator-opt/4
Para uma explicação, vamos começar com o 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 isso, substitui x
qualquer dígito hexadecimal y
aleatório por dados aleatórios (exceto forçar os 2 bits principais a10
conforme a especificação RFC), e o regex não corresponde aos caracteres -
ou 4
, portanto, ele não precisa lidar com eles. Muito, muito liso.
A primeira coisa a saber é que as chamadas de função são caras, assim como as expressões regulares (embora ele use apenas 1, ele possui 32 retornos de chamada, um para cada correspondência e, em cada um dos 32 retornos de chamada, chama Math.random () e v. toString (16)).
O primeiro passo para o desempenho é eliminar o RegEx e suas funções de retorno de chamada e usar um loop simples. Isso significa que temos que lidar com os caracteres -
e 4
, enquanto broofa não. Além disso, observe que podemos usar a indexação de String Array para manter sua arquitetura de modelo 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())
Basicamente, a mesma lógica interna, exceto que verificamos -
ou 4
, e usando um loop while (em vez dereplace()
retornos chamada) nos leva a uma melhoria quase 3X!
O próximo passo é pequeno no desktop, mas faz uma diferença decente no celular. Vamos fazer menos chamadas Math.random () e utilizar todos esses bits aleatórios em vez de jogar 87% deles fora com um buffer aleatório que é deslocado a cada iteração. Também vamos mover essa definição de modelo para fora do loop, para o caso de ajudar:
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())
Isso economiza de 10 a 30%, dependendo da plataforma. Não é ruim. Mas o próximo grande passo elimina completamente as chamadas da função toString com um clássico de otimização - a tabela de consulta. Uma tabela simples de pesquisa de 16 elementos executará o trabalho de toString (16) em muito menos tempo:
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())
A próxima otimização é outro clássico. Como estamos lidando apenas com 4 bits de saída em cada iteração de loop, vamos cortar o número de loops pela metade e processar 8 bits por iteração. Isso é complicado, pois ainda precisamos lidar com as posições de bits compatíveis com RFC, mas não é muito difícil. Em seguida, precisamos criar uma tabela de pesquisa maior (16x16 ou 256) para armazenar 0x00 - 0xff e construí-la apenas uma vez, fora da função 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())
Tentei um e6 () que processa 16 bits por vez, ainda usando o LUT de 256 elementos, e mostrava os retornos decrescentes de otimização. Embora tivesse menos iterações, a lógica interna era complicada pelo aumento do processamento, e executava o mesmo no desktop e apenas 10% mais rápido no celular.
A técnica final de otimização a ser aplicada - desenrole o loop. Como estamos repetindo um número fixo de vezes, tecnicamente podemos escrever tudo isso manualmente. Eu tentei isso uma vez com uma única variável aleatória r que continuei atribuindo novamente e o desempenho foi prejudicado. Porém, com quatro variáveis atribuídas a dados aleatórios antecipadamente, usando a tabela de pesquisa e aplicando os bits RFC apropriados, esta versão fuma todos eles:
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())
Modificado: http://jcward.com/UUID.js -UUID.generate()
O engraçado é que gerar 16 bytes de dados aleatórios é a parte mais fácil. O truque é expressá-lo no formato String com conformidade com RFC, e é realizado com mais precisão com 16 bytes de dados aleatórios, um loop não rotulado e uma tabela de pesquisa.
Espero que minha lógica esteja correta - é muito fácil cometer um erro nesse tipo de trabalho tedioso. Mas as saídas parecem boas para mim. Espero que você tenha gostado desse passeio louco pela otimização de código!
Esteja ciente: meu principal objetivo era mostrar e ensinar possíveis estratégias de otimização. Outras respostas abrangem tópicos importantes, como colisões e números verdadeiramente aleatórios, importantes para gerar bons UUIDs.