Necesito ejecutar millones de consultas del siguiente tipo.
Cada entrada consta de una pequeña colección (<100) de vectores booleanos de varios tamaños (<20000 elementos), cada uno con unos pocos 1 y muchos 0:
A = [ 0 0 0 1 0 0 0 0 0 0 0 ... ] B = [ 0 0 0 0 1 0 ... ] ...También tengo muchas (>20000) expresiones booleanas AND. Estas expresiones son constantes para todas las consultas.
S[1] = A[10] AND B[52] AND F[15] AND U[2] S[2] = I[8] AND Z[4] ...Cada expresión puede hacer referencia a cero o un elemento de cada vector. Las variables rara vez aparecen en más de una expresión. Para cada consulta, la salida es el conjunto de expresiones verdaderas.
¿Cuál es un buen algoritmo para calcular las consultas rápidamente, preferiblemente más rápido que evaluar cada expresión en orden? El algoritmo debe ejecutarse una vez para cada entrada, y hay millones de entradas para ejecutar, por lo que la velocidad es importante. Dado que las expresiones son constantes, puedo optimizarlas antes de tiempo. estoy trabajando con c
Regresa temprano. Tan pronto como encuentre un booleano falso, sabrá que la expresión and devolverá false , así que no verifique el resto.
En C, obtiene este comportamiento de forma predeterminada en expresiones booleanas codificadas:
(A[10] && B[52] && F[15] && U[2])Dependiendo de cuán predecibles sean las entradas, es posible que pueda obtener un gran rendimiento al clasificar las variables de cada expresión según la probabilidad de que sean falsas y al reordenar la expresión de la más probable a la menos probable.
Parece que estás usando muchos datos. Es una suposición, pero diría que obtendrá un comportamiento óptimo al preprocesar sus expresiones en pases óptimos de caché. Considere las dos expresiones dadas:
S[1] = A[10] AND B[52] AND F[15] AND U[2] S[2] = I[8] AND Z[4]reescribir estos como:
S[1] = 1; S[1] &= A[10]; S[1] &= B[52]; S[1] &= F[15]; S[1] &= U[2]; S[2] = 1; S[2] &= I[8]; S[2] &= Z[4];Luego ordene todas las expresiones juntas para crear una larga lista de operaciones:
S[1] = 1; S[2] = 1; S[1] &= A[10]; S[1] &= B[52]; S[1] &= F[15]; S[2] &= I[8]; S[1] &= U[2]; S[2] &= Z[4];Considere el tamaño del caché de la máquina disponible. Queremos todos los vectores de entrada en caché. Eso probablemente no pueda suceder, por lo que sabemos que estaremos extrayendo los vectores de entrada y los vectores de resultado dentro y fuera de la memoria varias veces. Queremos dividir el caché de la máquina disponible en tres partes: fragmento de vector de entrada, fragmento de vector de resultado y algún espacio de trabajo (de donde se extraerá nuestra lista actual de operaciones).
Ahora, recorra la lista de expresiones extrayendo expresiones que caen en el rango AI y S[1]-S[400]. Luego camine nuevamente tirando de JT (o lo que quepa en el caché) y extraiga esas operaciones a continuación, una vez que llegue al final de la lista de operaciones, repita para s[401]-s[800]. Este es el orden final de ejecución de las operaciones. Tenga en cuenta que esto se puede paralelizar sin contención a través de las bandas S.
La desventaja es que no obtienes el comportamiento de salida anticipada. La ventaja es que solo tiene fallas de caché a medida que realiza la transición de bloques de cómputo. Para un conjunto de datos tan grande, sospecho que esto (y la eliminación de todas las ramificaciones) abrumará la ventaja inicial.
Si aún desea intentar usar la optimización inicial, puede hacerlo, pero es más difícil de implementar. Considere: una vez que tenga su corchete de caché AI & S[1]-s[400], y haya creado una lista de operaciones en ese corchete:
S[1] &= A[10]; S[1] &= B[52]; S[1] &= F[15]; S[2] &= I[8];Luego puede reordenar las operaciones para agruparlas por S[x] (que este ejemplo ya era). Ahora, si encuentra que A[10] es falso, puede "salir temprano" al bloque S[2]. ¿En cuanto a cómo implementar esto? Bueno, sus operaciones ahora necesitan saber cuántos saltar hacia adelante desde la operación actual:
Operation[x ] => (S[1] &= A[10], on false, x+=3) Operation[x+1] => (S[1] &= B[52], on false, x+=2) Operation[x+2] => (S[1] &= F[15], on false, x+=1) Operation[x+3] => (S[2] &= I[8]...Nuevamente, sospecho que simplemente agregar la bifurcación será más lento que simplemente realizar todo el otro trabajo. Este no es un proceso inicial completo, ya que cuando pasa al siguiente bloque de entrada, tendrá que volver a inspeccionar cada valor de S[x] al que se accedió para determinar si ya ha fallado y debe omitirse.