Permutación Josefo - Seguimiento -- performance campo con tree campo con rust camp codereview Relacionados El problema

Josephus permutation - follow up


3
vote

problema

Español

Seguimiento de esta pregunta

cambios:

  1. El pop_min FUNCIÓN AHORA ANTIGUO Si el árbol está vacío.
  2. movió el size Atributo al Tree Struct.
  3. Aplicó la mayoría de las sugerencias dadas por las respuestas.
  4. agregó algunas otras funciones.
  5. refactore algunas funciones.
  6. movió todo al mismo archivo.
  extern crate itertools; use itertools::Unfold; use std::cmp::Ordering;   pub fn permutation(size: u32, m: u32) -> Box<Iterator<Item = u32>> {     let mut tree = Tree::new(1, size);     let x = Unfold::new(1, move |a| {         *a = (*a + m - 2) % tree.size + 1;         Some(tree.pop_rank(*a))     });     Box::new(x.take(size as usize)) }  #[derive(Debug)] struct Node {     data: u32,     left: Tree,     right: Tree, }  #[derive(Debug)] struct Tree {     size: u32,     root: Option<Box<Node>>, }  impl Tree {     fn new(from: u32, to: u32) -> Tree {         if from > to {             return Tree { root: None, size: 0, };         }         let mid = from + (to - from) / 2;         let node = Node {             data: mid,             left: Tree::new(from, mid - 1),             right: Tree::new(mid + 1, to),         };          Tree {             size: 1 + node.left.size + node.right.size,             root: Some(Box::new(node)),         }     }      fn find_rank(&mut self, rank: u32) -> &mut Tree {         let r = self.root                     .as_mut()                     .expect("rank out of range")                     .left                     .size + 1;          self.size -= 1;         match rank.cmp(&r) {             Ordering::Equal => self,             Ordering::Less => self.as_mut().left.find_rank(rank),             Ordering::Greater => self.as_mut().right.find_rank(rank - r),         }     }      fn as_mut(&mut self) -> &mut Box<Node> {         self.root.as_mut().unwrap()     }      fn as_ref(&self) -> &Box<Node> {         self.root.as_ref().unwrap()     }      fn empty(&self) -> bool {         self.root.is_none()     }      fn delete_node(&mut self) {         *self = self.root                     .take()                     .map(|mut x| {                         if x.left.empty() {                             x.right                         } else if x.right.empty() {                             x.left                         } else {                             x.data = x.right.pop_min();                             Tree {                                 root: Some(x),                                 size: self.size,                             }                         }                     })                     .expect("Can't delete None");     }      fn pop_min(&mut self) -> u32 {          if self.empty() {             panic!("underflow");         }          if self.as_ref().left.empty() {             let data = self.as_ref().data;             *self = self.root.take().unwrap().right;             data         } else {             self.size -= 1;             self.as_mut().left.pop_min()         }     }      fn pop_rank(&mut self, rank: u32) -> u32 {         let ranked = self.find_rank(rank);         let data = ranked.as_ref().data;         ranked.delete_node();         data     } }   
Original en ingles

Follow up of this question

Changes:

  1. The pop_min function now panics if the tree is empty.
  2. Moved the size attribute to the Tree struct.
  3. Applied most of the suggestions given by the answers.
  4. Added some other functions.
  5. Refactored some functions.
  6. Moved everything to the same file.
extern crate itertools; use itertools::Unfold; use std::cmp::Ordering;   pub fn permutation(size: u32, m: u32) -> Box<Iterator<Item = u32>> {     let mut tree = Tree::new(1, size);     let x = Unfold::new(1, move |a| {         *a = (*a + m - 2) % tree.size + 1;         Some(tree.pop_rank(*a))     });     Box::new(x.take(size as usize)) }  #[derive(Debug)] struct Node {     data: u32,     left: Tree,     right: Tree, }  #[derive(Debug)] struct Tree {     size: u32,     root: Option<Box<Node>>, }  impl Tree {     fn new(from: u32, to: u32) -> Tree {         if from > to {             return Tree { root: None, size: 0, };         }         let mid = from + (to - from) / 2;         let node = Node {             data: mid,             left: Tree::new(from, mid - 1),             right: Tree::new(mid + 1, to),         };          Tree {             size: 1 + node.left.size + node.right.size,             root: Some(Box::new(node)),         }     }      fn find_rank(&mut self, rank: u32) -> &mut Tree {         let r = self.root                     .as_mut()                     .expect("rank out of range")                     .left                     .size + 1;          self.size -= 1;         match rank.cmp(&r) {             Ordering::Equal => self,             Ordering::Less => self.as_mut().left.find_rank(rank),             Ordering::Greater => self.as_mut().right.find_rank(rank - r),         }     }      fn as_mut(&mut self) -> &mut Box<Node> {         self.root.as_mut().unwrap()     }      fn as_ref(&self) -> &Box<Node> {         self.root.as_ref().unwrap()     }      fn empty(&self) -> bool {         self.root.is_none()     }      fn delete_node(&mut self) {         *self = self.root                     .take()                     .map(|mut x| {                         if x.left.empty() {                             x.right                         } else if x.right.empty() {                             x.left                         } else {                             x.data = x.right.pop_min();                             Tree {                                 root: Some(x),                                 size: self.size,                             }                         }                     })                     .expect("Can't delete None");     }      fn pop_min(&mut self) -> u32 {          if self.empty() {             panic!("underflow");         }          if self.as_ref().left.empty() {             let data = self.as_ref().data;             *self = self.root.take().unwrap().right;             data         } else {             self.size -= 1;             self.as_mut().left.pop_min()         }     }      fn pop_rank(&mut self, rank: u32) -> u32 {         let ranked = self.find_rank(rank);         let data = ranked.as_ref().data;         ranked.delete_node();         data     } } 
        

Lista de respuestas

3
 
vote
vote
La mejor respuesta
 

Esto se ve principalmente bien, pero realmente no me gusta

  cancel5  

Puede cambiar el cancel6 para obtener

  cancel7  

que ya es mucho mejor. Luego lidiar con el error y el nombramiento:

  cancel8  

Luego me lo aplano hacia fuera hacia fuera

  cancel9  

Las herramientas de Rust son geniales, pero no deben ser abusadas. En este caso, un simple estilo imperativo lee mucho mejor que las transformaciones semifuncionales, por lo que se debe preferir.

La deriva más hacia la derecha se puede tratar en ByRef0 :

  ByRef1  

Ahora, ByRef2 está haciendo algo realmente horrible:

  ByRef3  

Este se rompe el ByRef4 's invariantes. Mucho mejor sería llamar a este ByRef5 y mover ByRef6 dentro de ella. Puede hacer esto un poco mejor al tener ByRef7 devuelva un ByRef8 y solo dependiendo de eso, sin embargo.

Entonces tenemos una buena cadena de funciones

  ByRef9  

La restauración del Public Event CloseCommand(ByVal sender As CustomerGroupsView, Optional ByRef Cancel As Boolean = False) Public Event EditCustomerGroupCommand(ByVal id As Long, ByVal description As String, Optional ByRef Cancel As Boolean = False) Public Event AddCustomerGroupCommand(Optional ByRef Cancel As Boolean = False) Public Event DeleteCustomerGroupCommand(ByVal id As Long, Optional ByRef Cancel As Boolean = False) 0 Antes de llamar a Public Event CloseCommand(ByVal sender As CustomerGroupsView, Optional ByRef Cancel As Boolean = False) Public Event EditCustomerGroupCommand(ByVal id As Long, ByVal description As String, Optional ByRef Cancel As Boolean = False) Public Event AddCustomerGroupCommand(Optional ByRef Cancel As Boolean = False) Public Event DeleteCustomerGroupCommand(ByVal id As Long, Optional ByRef Cancel As Boolean = False) 2 puede aumentar ligeramente la carga computacional, pero significa que cada función solo se basa en la estructura de datos. Invariantes básicos para trabajar correctamente.

Nota que Public Event CloseCommand(ByVal sender As CustomerGroupsView, Optional ByRef Cancel As Boolean = False) Public Event EditCustomerGroupCommand(ByVal id As Long, ByVal description As String, Optional ByRef Cancel As Boolean = False) Public Event AddCustomerGroupCommand(Optional ByRef Cancel As Boolean = False) Public Event DeleteCustomerGroupCommand(ByVal id As Long, Optional ByRef Cancel As Boolean = False) 33 no es realmente Public Event CloseCommand(ByVal sender As CustomerGroupsView, Optional ByRef Cancel As Boolean = False) Public Event EditCustomerGroupCommand(ByVal id As Long, ByVal description As String, Optional ByRef Cancel As Boolean = False) Public Event AddCustomerGroupCommand(Optional ByRef Cancel As Boolean = False) Public Event DeleteCustomerGroupCommand(ByVal id As Long, Optional ByRef Cancel As Boolean = False) 4 Como es capturado por Public Event CloseCommand(ByVal sender As CustomerGroupsView, Optional ByRef Cancel As Boolean = False) Public Event EditCustomerGroupCommand(ByVal id As Long, ByVal description As String, Optional ByRef Cancel As Boolean = False) Public Event AddCustomerGroupCommand(Optional ByRef Cancel As Boolean = False) Public Event DeleteCustomerGroupCommand(ByVal id As Long, Optional ByRef Cancel As Boolean = False) 5 Auto-Panic.

Public Event CloseCommand(ByVal sender As CustomerGroupsView, Optional ByRef Cancel As Boolean = False) Public Event EditCustomerGroupCommand(ByVal id As Long, ByVal description As String, Optional ByRef Cancel As Boolean = False) Public Event AddCustomerGroupCommand(Optional ByRef Cancel As Boolean = False) Public Event DeleteCustomerGroupCommand(ByVal id As Long, Optional ByRef Cancel As Boolean = False) 6 y Public Event CloseCommand(ByVal sender As CustomerGroupsView, Optional ByRef Cancel As Boolean = False) Public Event EditCustomerGroupCommand(ByVal id As Long, ByVal description As String, Optional ByRef Cancel As Boolean = False) Public Event AddCustomerGroupCommand(Optional ByRef Cancel As Boolean = False) Public Event DeleteCustomerGroupCommand(ByVal id As Long, Optional ByRef Cancel As Boolean = False) 7 son nombres engañosos ya que realmente dan asas a Public Event CloseCommand(ByVal sender As CustomerGroupsView, Optional ByRef Cancel As Boolean = False) Public Event EditCustomerGroupCommand(ByVal id As Long, ByVal description As String, Optional ByRef Cancel As Boolean = False) Public Event AddCustomerGroupCommand(Optional ByRef Cancel As Boolean = False) Public Event DeleteCustomerGroupCommand(ByVal id As Long, Optional ByRef Cancel As Boolean = False) 8 ; Mejor sería Public Event CloseCommand(ByVal sender As CustomerGroupsView, Optional ByRef Cancel As Boolean = False) Public Event EditCustomerGroupCommand(ByVal id As Long, ByVal description As String, Optional ByRef Cancel As Boolean = False) Public Event AddCustomerGroupCommand(Optional ByRef Cancel As Boolean = False) Public Event DeleteCustomerGroupCommand(ByVal id As Long, Optional ByRef Cancel As Boolean = False) 9 y Option Explicit0 . También cambiaría Option Explicit1 para Option Explicit2 para Clarity.

También cambiaría Option Explicit3 a Option Explicit4 en Option Explicit5 .


He estado transferido por este problema por un tiempo ahora, así que voy a presentar mis pensamientos al respecto. muy poco de lo que sigue tiene algo que ver con su implementación, pero con suerte, alguien al menos lo encuentre un poco interesante.

Vale la pena señalar que Option Explicit6 -HEANTES Las estructuras tienden a ser lentas. Una estructura de árbol mucho más eficiente para trabajar podría ser un Option Explicit7 así:

  Option Explicit8  

Option Explicit9 se utilizan para guiarlo a un índice en la parte inferior. "Usando" el quinto valor dará como resultado el árbol modificado así

  vwCustomerGroups0  

Luego, vale la pena señalar que nunca necesita leer las ramas correctas en el recorrido, por lo que aquellos pueden ser descartados:

  vwCustomerGroups1  

Luego, mueva el tamaño en la estructura de datos en sí, ya que debe ser leída con frecuencia en el algoritmo y en realidad durante el recorrido.

  vwCustomerGroups2  

He encontrado que solo esto da un factor de aceleración notable pero decreciente de 3-4x; Estaba esperando con todo el vwCustomerGroups3 para que eso esté más cerca de 10. El problema parece ser que las ramas siguen siendo impredecibles y el comportamiento sigue golpeando el caché; La mejora principal no llegó del cambio en el formato, sino que no está escribiendo a la rama derecha (no la etapa de elaboración, pero simplemente no le escribo). Además, es probable que los asignadores pongan todos esos valores en caja uno junto al otro, y la falta de eficiencia espacial realmente no le duele al hacer el acceso psudeo-aleatorio de todos modos.

Entonces tenga en cuenta que con algo así como un (14345577, 15347) -permutación, $ log 14345577 $ es mucho mayor que $ log 15347 $. Puedes usar esto a tu ventaja ignorando la parte superior del árbol:

  vwCustomerGroups4  

Puede descartar vwCustomerGroups5 ; Normalmente, los saltos no serán más grandes que los pasos de 15347 veces la "escaseidad" de los datos. Cuando la escasez se vuelve tan grande que usted es Tomando saltos tan grandes, puede "volver a comprar" la matriz completa en el tiempo de $ O (n) $. Dado que solo necesita hacer esto cuando la densidad cae en algún lugar por debajo del 50%, esta es una forma relativamente barata de pasar de un total $ O (n log n) $ a $ O (n log m) $ TIEMPO La complejidad (cambiar el tamaño de cada vez que las mitades de la capacidad resulta en solo $ O (n) $ trabajo adicional).

Tenga en cuenta que el nivel superior se convierte en un montón de raíces en los sub-árboles , no es un solo árbol. Así no puedes ignorar los nodos correctos, o estarás perdiendo El tamaño del árbol. Conceptualmente, esos nodos correctos se calculan desde el nodo anterior, por lo que si no hay ningún nodo anterior, no puede eliminar el valor.

Un ejemplo extremo sería un (10000000, 3) -permutación, donde todo el árbol está claramente demasiado exagerado. Este recorte de árboles funciona bien allí.

Entonces vale la pena mirar esas filas inferiores, para ver si podemos hacer algo más inteligente sobre ellos:

  vwCustomerGroups6  

A pesar de las sucursales de la derecha, esto sigue siendo un montón de datos para expresar 64 bits de datos. De hecho, utiliza 63 veces esa cantidad de datos incluso después de la elisión. Mucho más efectivo es tener un entero real de 64 bits.

  vwCustomerGroups7  

Esto todavía puede ser de manera eficiente binaria buscada utilizando una rutina especializada, turnos de bits, máscaras y vwCustomerGroups8 . De hecho, en nuevos procesadores X86, ni siquiera tiene que hacerlo; señala Jukka Suomela que es tan simple como

  vwCustomerGroups9  

Lamentablemente, creo que ahora mismo necesito usar customerGroups0 , pero eso no es un show-stopper. Si customerGroups1 no está disponible, la búsqueda binaria con customerGroups2 es bastante rápido, pero ciertamente no es casi como rápido.

De hecho, esto también hace que la etapa de compactación necesitamos para elegir la parte superior del árbol mucho más sencilla; El conjunto de bits actúa como una máscara sobre nuestros valores que podemos simplemente iterar.


Calculemos rápidamente lo que es la sobrecarga. Todo nuestro árbol parece

  customerGroups3  

customerGroups4 Costos A customerGroups5 por valor de salida, pero en realidad se puede ignorar hasta la primera compactación como antes, es un mapa de identidad ( 99887776555443366 al principio).

customerGroups7 introduce un poco más por valor.

Si mira customerGroups8 y customerGroups9 puede ver que puede "deslizar" el " "0 s de las ramas derecha Para llenar exactamente una fila (reutilizaré esta propiedad más tarde). El nivel más abajo (código> 99887766555443371 está en el nivel 128 (ya que 99887766555443372 funciona en el nivel de 64), por lo que es un uso de bits gastado por 128 valores.

Ergo Tenemos 1-2 bits adicionales por valor, y solo la mitad de los valores realmente deben almacenarse. Por lo tanto, nuestra complejidad del espacio está bastante cerca de " "3 . Si se espera que " "4 se aborde " "5 en tamaño o espacio importa más que el movimiento desde $ O (n log n) $ a $ O (n log m) $, en su lugar, puede usar " "6 sin recortar la parte superior del árbol, por lo que no se necesita una matriz , para obtener un costo de espacio de aproximadamente solo " "8 bits.

Se podría obtener un híbrido agradable haciendo " "9 un poco más alto, por lo que uno tiene que cambiar el tamaño ligeramente menos con frecuencia. Dos capas adicionales significa dos ramas adicionales por iteración, pero los retrasos del tamaño hasta que la densidad se trata de aproximadamente 1/8, dejando el consumo de memoria de aproximadamente TAB0 mientras aún tiene $ o (n log m) $ Operaciones. Además, hacer el primer cambio de tamaño Temprano Dolor ya que antes de que TAB1 no se usa y los cambios posteriores son más pequeños. Por otro lado, algunas ramas adicionales son caras cuando las ramas son impredecibles, por lo que en realidad tiene sentido mantener el árbol superficial y hacer una búsqueda lineal sobre las raíces. Vale la pena señalar que el cambio de tamaño puede hacer que el árbol sea más pequeño y es realmente caro, por lo que el comercio para hacerlo más tarde no es tan claro. Los parámetros precisos son sintonizables.

Esto está empezando a sentirse óptimo, ya que $ log M $ se trata del costo más pequeño para un salto que normalmente se puede esperar razonablemente y nos estamos acercando a un pequeño número de bits de sobrecarga. Sin embargo, podríamos poder hacer que la estructura de datos sea más óptima. Una posible mejora es tomar la vista "compacta" del árbol que utilizamos para calcular su tamaño: deslice cada valor por el "Diapositiva derecha":

  TAB2  

para obtener

  TAB3  

Tenga en cuenta que Traversal es ahora solo una búsqueda binaria sobre un rango de subservicio de la matriz. Esto podría parecer difícil de atravesar, pero en realidad es una transformación bastante mecánica.

generando esto es más difícil, pero tiene la propiedad encantadora que cada módulo es

  TAB4  

que puede tomar el $ log_2 $ para obtener

  TAB5  

OEIS agrega útil que ,

blockquote>

a (n) es el número de 0 al final de n cuando n se escribe en la base 2

y luego observa que TAB6 le da cuántas veces antes ha visto este valor, lo que le permite lidiar con las partes sobrantes.

Tenga en cuenta que el módulo es para una raíz "recortada", por lo que debe tomar el 99887766655443387 contra el módulo de nivel superior que está usando, y también necesita agregar 7 antes de ese momento para multiplicar todos los módulos por 128 (porque los grupos de fondo de 64 se tratan en TAB8 ).


Con mi implementación, por muy grande $ M $ (mismo orden de magnitud como $ n $), la poda del árbol es totalmente ineficaz (y es simplemente inútil sobrecarga), por lo que las únicas ganancias de rendimiento provienen de la Cambio en cómo ocurre la iteración, el movimiento a la iteración sobre un vector contiguo y el 99887776655443389 la matriz que quita las últimas capas del árbol. El movimiento a un vector contiguo es en realidad bastante ineficaz como un potenciador de velocidad, ya que el vwCutstomerGroups_AddCustomerGroupCommand()0

Para "Medio", aislado $ M $ (por ejemplo, $ n $ es 14345577, $ M $ es 15347), la poda de árbol entra en juego y la velocidad mejora el 3 veces. Su código no, por lo que aumenta la ventaja de la velocidad. Hay aproximadamente una mejora de factor 20 sobre su código aquí.

por pequeños $ M $ (por ejemplo, 3), los empujes de poda de árboles aumentan aún más la velocidad significativamente, lo que resulta en disminuciones masivas en el tiempo. Además, las instrucciones de IMC en la parte inferior del árbol terminan significativamente mejorando. Sin embargo, su código también se vuelve muy predecible, ya que las ramas se repiten con frecuencia, por lo que el costo que realmente cae también y la mejora en general de la velocidad es tan grande como la reducción del tiempo sola. Hay aproximadamente una mejora de factor 32 sobre su código aquí.


Honestamente, esperaba más, pero puedo ver por qué las mejoras eran solo esta buena. Debería ser posible mejorar esto por el uso de un árbol B en lugar de un árbol binario, reemplazando efectivamente

  vwCutstomerGroups_AddCustomerGroupCommand()1 

con

  vwCutstomerGroups_AddCustomerGroupCommand()2  

que debe mejorar la previsibilidad de la rama mediante el uso de búsquedas lineales interrelacionadas intermitentes. Sin embargo, la reducción de hacer esto, la forma "obvia" es leve a medida que aún terminas con ramas mal predecibles sobre las ubicaciones de propagación.

Idealmente, uno debe reorganizar los valores como

  vwCutstomerGroups_AddCustomerGroupCommand()3  

en su lugar.

Otro supervisión que he estado ignorando a propósito es SIMD. Los pequeños vectores de SIMD para vwCutstomerGroups_AddCustomerGroupCommand()4 Los datos muestran esto relativamente débil, pero bajando el árbol 99887776655443395 es extremadamente útil y básicamente hace un árbol B de 8 de ancho óptimo. Las nuevas instrucciones AVX se harían mucho mejor, y permitir que todo el árbol sea fuertemente vectorizado. Sin embargo, voy a seguir ignorando esto, porque 'Causa Simd con óxido no es ideal en este momento.

 

This mostly looks OK, but I really don't like

*self = self.root             .take()             .map(|mut x| {                 if x.left.empty() {                     x.right                 } else if x.right.empty() {                     x.left                 } else {                     x.data = x.right.pop_min();                     Tree {                         root: Some(x),                         size: self.size,                     }                 }             })             .expect("Can't delete None"); 

You can shift up the expect to get

let mut node = self.root.take().expect("Can't delete None"); *self =     if node.left.empty() {         node.right     } else if node.right.empty() {         node.left     } else {         node.data = node.right.pop_min();         Tree {             root: Some(node),             size: self.size,         }     }; 

which is already much nicer. Then deal with the error and naming:

fn delete_root_node(&mut self) {     let mut root = self.root.take().expect("Empty tree has no root");     *self =         if root.left.empty() {             root.right         } else if root.right.empty() {             root.left         } else {             root.data = root.right.pop_min();             Tree {                 root: Some(root),                 size: self.size,             }         }; } 

Then I'd flatten it back out

fn delete_root_node(&mut self) {     let mut root = self.root.take().expect("Empty tree has no root");      if root.left.empty() {         *self = root.right     } else if root.right.empty() {         *self = root.left     } else {         root.data = root.right.pop_min();         self.root = Some(root);     }; } 

Rust's tools are great, but they shouldn't be abused. In this case, a simple imperative style reads much nicer than the semi-functional transformations, so should be preferred.

More rightward drift can be dealt with in find_rank:

fn find_rank(&mut self, rank: u32) -> &mut Tree {     let right_offset = self.as_mut().left.size + 1;      self.size -= 1;     match rank.cmp(&right_offset) {         Ordering::Equal => self,         Ordering::Less => self.as_mut().left.find_rank(rank),         Ordering::Greater => self.as_mut().right.find_rank(rank - right_offset),     } } 

Now, find_rank is doing something really horrible:

self.size -= 1; 

This breaks the Tree's invariants. Much better would be to call this pop_rank and move pop_rank inside of it. You can make this a bit nicer by having pop_root return a u32 and just depending on that, though.

Then we have a nice chain of functions

fn pop_rank(&mut self, rank: u32) -> u32 {     let left_size = self.as_mut().left.size;     self.size -= 1;      match rank.cmp(&(1 + left_size)) {         Ordering::Equal   => {             self.size += 1;             self.pop_root()         },         Ordering::Less    => self.as_mut().left.pop_rank(rank),         Ordering::Greater => self.as_mut().right.pop_rank(rank - 1 - left_size),     } }  fn pop_root(&mut self) -> u32 {     let mut root = self.root.take().expect("Empty tree has no root");     let data = root.data;      if root.left.empty() {         *self = root.right     } else if root.right.empty() {         *self = root.left     } else {         root.data = root.right.pop_min();         self.root = Some(root);         self.size -= 1;     };      data }  fn pop_min(&mut self) -> u32 {     if self.as_ref().left.empty() {         let data = self.as_ref().data;         *self = self.root.take().unwrap().right;         data     } else {         self.size -= 1;         self.as_mut().left.pop_min()     } } 

The restoring of self.size in pop_rank before calling pop_root might slightly increase computational burden but it means that each function only relies on the data structure's basic invariants to work properly.

Note that pop_min doesn't actually need to panic! as that's captured by self.as_ref's auto-panic.

as_ref and as_mut are misleading names since they actually give handles to root; better would be get_root and get_root_mut. I'd also swap pop_min for pop_first for clarity.

I'd also change m to step in permutation.


I've been transfixed by this problem for a while now, so I'm going to introduce my thoughts about it. Very little of what follows has anything to do with your implementation, but hopefully someone at least finds it a little interesting.

It's worth noting that Box-heavy structures tend to be slow. A much more efficient tree structure to work with could be a Vec<Vec<usize>> like so:

tree[0]               13 tree[1]        8               5 tree[2]    4       4       4       1 tree[3]  2   2   2   2   2   2   1 tree[4] 1 1 1 1 1 1 1 1 1 1 1 1 1 

tree[0..3] are used to guide you to an index in the bottom. "Using" the fifth value would result in the tree modified like so

tree[0]               12 tree[1]        7               5 tree[2]    4       3       4       1 tree[3]  2   2   1   2   2   2   1 tree[4] 1 1 1 1 0 1 1 1 1 1 1 1 1 

Then it's worth noting that you never actually need to read the right branches on traversal, so those can be discarded:

tree[0]               13 tree[1]        8               . tree[2]    4       .       4       . tree[3]  2   .   2   .   2   .   1 tree[4] 1 . 1 . 1 . 1 . 1 . 1 . 1 

Then move the size into the data structure itself, since that needs to be read frequently in the algorithm and not actually during traversal.

tree[0]        8               . tree[1]    4       .       4       . tree[2]  2   .   2   .   2   .   1 tree[3] 1 . 1 . 1 . 1 . 1 . 1 . 1 

I've found that just this gives a noticeable but underwhelming speed-up factor of 3-4x; I was expecting with all the Boxing for that to be closer to 10. The problem seems to be that the branches are still unpredictable and the behaviour is still cache-thrashing; the main improvement came not from the change in format but from not writing to the right branch (not the eliding step but just not writing to it). Plus, allocators are likely to put all those boxed values next to each other, and the lack of space efficiency doesn't really hurt when doing psuedo-random access anyway.

Then note that with something like a (14345577, 15347)-permutation, \$\log 14345577\$ is much greater than \$\log 15347\$. You can use this to your advantage by ignoring the top of the tree:

tree[ 0]             8388608 ... tree[ 1]           4194304 ... ... tree[ 8]       32768 ... tree[ 9]     16384 ... ... tree[22]   2 ... tree[23] 1 ... 

You can actually discard tree[0..8]; jumps are normally going to be no greater than steps of 15347 times the "sparsity" of the data. When the sparsity gets so large that you are taking jumps that large, you can "recompact" the whole array in \$O(n)\$ time. Since you only need to do this when density drops somewhere below 50%, this is a relatively cheap way of moving from a total \$O(n \log n)\$ to \$O(n \log m)\$ time complexity (resizing each time capacity halves results in only \$O(n)\$ extra work).

Note that the top level then becomes a bunch of roots into sub-trees, not a single tree. Thus you can't ignore the right-nodes, or you'll be losing the size of the tree. Conceptually, those right nodes are calculated from the node above, so if there's no node above you aren't able to drop the value.

An extreme example would be a (10000000, 3)-permutation, where the entire tree is clearly overkill. This tree trimming works well there.

Then it's worth looking at those bottom rows, to see if we can do something more intelligent about them:

tree[X]                            64   ... ... tree[X]    4       4       4       4    ... tree[X]  2   .   2   .   2   .   2   .  ... tree[X] 1 . 1 . 1 . 1 . 1 . 1 . 1 . 1 . ... 

Despite the elided right branches, this is still a ton of data to express 64 bits of data. In fact, it uses 63 times that amount of data even after elision. Much more effective is to have an actual 64 bit integer.

0b1111111111111111111111111... 

This can still be efficiently binary searched by utilizing a specialized routine, bit shifts, masks and u64::count_ones. In fact, on newer x86 processors you don't even have to; Jukka Suomela points out that it's as simple as

pub fn position_nth_set_bit(value: u64, mut bit: u32) -> u32 {     // Holy smokes     bmi_pdep_64(1 << bit, value).trailing_zeros() } 

Sadly I think right now I need to use asm!, but that's no show-stopper. If pdep isn't available, the binary search with count_ones is still pretty fast - but it's certainly not nearly as fast.

In fact, this also makes the compacting step that we need to elide the top of the tree much simpler; the bit-set acts as a mask over our values that we can just iterate.


Let's quickly calculate what the overhead is. Our entire tree looks like

roots           x              x tree[X]     x       .       x tree[X]   x   .   x   .   x   . tree[X]  x . x . x . x . x . x . masks    bbbbbbbbbbbbbbbbbbbbbbbb values   VVVVVVVVVVVVVVVVVVVVVVVV  xe2x86x90 We don't need this until after compacting 

values costs a usize per output value, but can actually be ignored until the first compaction as before then it's an identity map (values[k] == k at first).

masks introduces one further bit per value.

If you look at tree and roots the right way, you can see you can "slide" the xs down the right branches to fill exactly one row (I'll re-use this property later). The bottom-most tree[X] is at the 128 level (since masks.count_ones() works at the 64 level), so that's a usize of bits spent per 128 values.

Ergo we have 1-2 extra bits per value, and only half of the values actually need to be stored. So our space complexity is pretty close to n * usize::BITS / 2. If m is expected to approach n in size or space matters more than the move from \$O(n \log n)\$ to \$O(n \log m)\$, you can instead use masks without trimming the top of the tree, so no values array is needed, to get a space cost of roughly just 2 * n bits.

A pleasing hybrid might be obtained by making tree a little bit taller, so one has to resize slightly less frequently. Two extra layers means two extra branches per iteration but delays resizing until the density is about 1/8, leaving memory consumption of about n * usize::BITS / 8 while still having \$O(n \log m)\$ operations. Plus, doing the first resize early hurts as before it values is not used and the later resizes are smaller. On the other hand, a few extra branches is expensive when the branches are unpredictable, so it actually makes sense to keep the tree shallow and do a linear search over the roots. It's worth noting that resizing can make the tree smaller and isn't actually expensive, so the trade to do it later isn't so clear-cut. The precise parameters are tunable.

This is starting to feel optimal, since \$\log m\$ is about the smallest cost for a jump one can normally reasonably expect and we're approaching a small number of bits of overhead. However, we might be able to make the data structure more optimal. One possible improvement is to take the "compact" view of the tree we used to calculate its size: slide each value down the "right-hand slide":

roots           a              b tree[X]     c       xe2x86x98       d tree[X]   e   xe2x86x98   f   xe2x86x98   g   xe2x86x98 tree[X]  x xe2x86x98 x xe2x86x98 x xe2x86x98 x xe2x86x98 x xe2x86x98 x xe2x86x98 masks    bbbbbbbbbbbbbbbbbbbbbbbb values   VVVVVVVVVVVVVVVVVVVVVVVV 

to get

tree     x e x c x f x a x g x d xc2xb7 xc2xb7 xc2xb7 b masks    bbbbbbbbbbbbbbbbbbbbbbbb values   VVVVVVVVVVVVVVVVVVVVVVVV 

Note that traversal is now just a binary search over a sub-range of the array. This might look tough to traverse, but it's actually a fairly mechanical transformation.

Generating this is more tough, but it does have the lovely property that each modulo is

1, 2, 1, 4, 1, 2, 1, 8, 1, 2, 1, 4, 1, 2, ... 

which you can take the \$\log_2\$ of to get

0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, ... 

OEIS helpfully adds that,

a(n) is the number of 0's at the end of n when n is written in base 2

and then you note that n >> (n.trailing_zeros() + 1) gives how many times you've seen this value before, which lets you deal with the leftover parts.

Note that the modulo is for a "cropped" root, and so you need to take the min against the top-level modulo you're using, and you also need to add 7 before then to multiply all the modulos by 128 (because the bottom groups of 64 are dealt with in masks).


With my implementation, for very large \$m\$ (same order of magnitude as \$n\$), the tree pruning is entirely ineffective (and is just pointless overhead), so the only performance gains come from the change in how iteration happens, the move to iteration over a contiguous vector and the masks array removing the last few layers of the tree. The move to a contiguous vector is actually rather ineffective as a speed enhancer since the Boxes are probably contiguous anyway and the major overhead is in the unpredictable branching. There's roughly a factor 6 improvement over your code here.

For "medium"-sized \$m\$ (eg. \$n\$ is 14345577, \$m\$ is 15347), tree pruning comes into play and speed improves 3-fold. Your code does not, so the speed advantage increases. There's roughly a factor 20 improvement over your code here.

For small \$m\$ (eg. 3), the tree pruning pushes further increases speed significantly, resulting in massive decreases in time. Further, the BMI instructions at the bottom of the tree end up improving speed significantly. However, your code also becomes very predictable, since branches repeat frequently, so the cost there actually falls too and the overall relative speed improvement is not as great as the time reduction alone would imply. There's roughly a factor 32 improvement over your code here.


Honestly I was hoping for more, but I can see why the improvements were only this good. It should be possible to improve this a lot by using a B-tree instead of a binary tree, by effectively replacing

roots           x              x tree[X]     x       .       x tree[X]   x   .   x   .   x   . tree[X]  x . x . x . x . x . x . masks    bbbbbbbbbbbbbbbbbbbbbbbb values   VVVVVVVVVVVVVVVVVVVVVVVV 

with

roots       x       x       x       x tree[X]       .       .       . tree[X]  x x x . x x x . x x x . masks    bbbbbbbbbbbbbbbbbbbbbbbb values   VVVVVVVVVVVVVVVVVVVVVVVV 

which should improve branch predictability by using intermittent unrolled linear searches. However, the reduction from doing this the "obvious" way is slight as you still end up with poorly predictable branches over spread-out locations.

Ideally, one should reorganize the values as

roots                      x x x x tree[X]  x x x|x x x|x x x| masks    bbbbbbbbbbbbbbbbbbbbbbbb values   VVVVVVVVVVVVVVVVVVVVVVVV 

instead.

Another oversight I've been purposefully ignoring is SIMD. Small SIMD vectors for usize-sized data makes this relatively weak, but lower down the tree a u16x8 vector is likely extremely useful, and basically makes an 8-wide B-tree optimal. Newer AVX instructions would do far better, and allow the whole tree to be heavily vectorized. I'm going to continue to ignore this, though, 'cause SIMD with Rust is not ideal right now.

 
 
 
 

Relacionados problema

8  Reemplace la cadena en el archivo  ( Replace string in file ) 
Este es mi primer módulo que escribí en el óxido. Soy nuevo en el idioma y no pude encontrar una manera de reemplazar fácilmente algunas palabras en un archiv...

3  Lista doblemente vinculada en óxido usando los punteros crudos  ( Doubly linked list in rust using raw pointers ) 
Estoy practicando la oxidación escribiendo una lista doblemente vinculada usando los punteros crudos, 9988776655544330 Para asignar datos en el montón, 998...

3  Base64 String ↔ Array flotante  ( Base64 string %e2%86%94 float array ) 
Necesito convertir move constructor8 matrices con una longitud de fix a base64 Representación y atrás. Mi código actual se ve así. Funciona, pero se sient...

5  Canción de 99 cervezas en óxido  ( 99 beers song in rust ) 
Estoy aprendiendo a Rust. Me parece que a veces se confuso en comparación con otros lenguajes de programación, especialmente cuando se trata de cadenas y re...

7  Implementar una secuencia de Fibonacci genérica en óxido sin usar copia rasgo  ( Implement a generic fibonacci sequence in rust without using copy trait ) 
Estoy tratando de aprender a óxido y soy un principiante. ¿Cómo se hace frente a la implementación de una versión genérica de la secuencia FIBONACCI sin usar ...

3  Aplicación singularidad e IPC unilateral en UNIX  ( Application uniqueness and unilateral ipc on unix ) 
este programa Detecta la singularidad de la aplicación, si la aplicación es una instancia única / primaria, lanza un servidor, de lo contrario, un cliente...

6  Clasificación de palabras por frecuencia  ( Sorting words by frequency ) 
Estoy haciendo una tarea simple en óxido después de leer el Libro de óxido : Lea un archivo de texto dividirlo en Whitespace desinfectar palabras elim...

6  Utilidad X-up para EVE Online  ( X up utility for eve online ) 
Estoy aprendiendo a Rust. También interpreto a Eve Online: un videojuego sobre las naves espaciales de Internet. Decidí que sería divertido practicar el óxi...

1  La mejor implementación de la mejor búsqueda de primera búsqueda en óxido  ( Greedy best first search implementation in rust ) 
He implementado un codicioso algoritmo de primera búsqueda en Rust, ya que no pude encontrar una ya implementada en las cajas existentes. Tengo un pequeño pro...

7  HMAC con SHA256 / 512 en óxido  ( Hmac with sha256 512 in rust ) 
He intentado lo mejor que puedo, para implementar HMAC como se especifica en la RFC 2104 < / a>. Este es también mi primer proyecto de óxido, por lo que las ...




© 2022 respuesta.top Reservados todos los derechos. Centro de preguntas y respuestas reservados todos los derechos