regulares manejo interpolar expresiones cadenas ruby optimization profiling

manejo - interpolar en ruby



¿Por qué el conteo de letras es más rápido usando el conteo de Cadena#que usando caracteres de cadena#en Ruby? (4)

Lo que es lento es group_by. Efectivamente, aunque necesita hacer 4 pases para el método de conteo, el método group_by es mucho más lento porque está haciendo mucho trabajo para hacer ese group_by.

Rompiendo el código un poco para tener un método que solo haga el grupo:

def create_genome "gattaca" * 100 end def count_frequency_using_chars(sequence) 100000.times do sequence.chars.group_by{|x| x}.map{|letter, array| [letter, array.count]} end end def count_frequency_using_count(sequence) 100000.times do ["a", "c", "g", "t"].map{|letter| sequence.count(letter)} end end def just_group_by(sequence) 100000.times do sequence.chars.group_by{|x| x} end end sequence = create_genome ... ruby-1.9.2-p180 :068 > puts Time.now() 2011-06-17 11:17:36 -0400 ruby-1.9.2-p180 :069 > count_frequency_using_chars(sequence) => 100000 ruby-1.9.2-p180 :070 > puts Time.now() 2011-06-17 11:18:07 -0400 ruby-1.9.2-p180 :071 > count_frequency_using_count(sequence) => 100000 ruby-1.9.2-p180 :072 > puts Time.now() 2011-06-17 11:18:08 -0400 ruby-1.9.2-p180 :073 > just_group_by(sequence) => 100000 ruby-1.9.2-p180 :074 > puts Time.now() 2011-06-17 11:18:37 -0400

Puedes ver

  • usar group_by tomó 31 segundos
  • usar el conteo tomó 1 segundo
  • solo haciendo group_by tomó 29 segundos

Si bien usar group_by para obtener la información que necesita es bueno conceptualmente, está haciendo un trabajo adicional que no necesita hacer.

Usando el siguiente punto de referencia:

def create_genome "gattaca" * 100 end def count_frequency_using_chars(sequence) 100000.times do sequence.chars.group_by{|x| x}.map{|letter, array| [letter, array.count]} end end def count_frequency_using_count(sequence) 100000.times do ["a", "c", "g", "t"].map{|letter| sequence.count(letter)} end end sequence = create_genome count_frequency_using_chars(sequence) count_frequency_using_count(sequence)

Descubrí que, en Ruby basado en C para 1.8 y 1.9.2, el uso del String#count(letter) es aproximadamente 50 veces más rápido que ordenarlos y contarlos usando Enumerable#group_by y Array#count . Me sorprendió un poco, porque el método de String#count lee la cadena cuatro veces en cada iteración, mientras que el último solo lo lee una vez.

Traté de ejecutar el código en ruby-prof y perftools.rb, y ambos meramente indicaron que String#chars tomó el 90% del tiempo, sin desglosar dónde pasó el 90% del tiempo.

Si tuviera que adivinar por qué hay una diferencia, diría que la creación de 70 millones de cadenas de un solo carácter sería costosa, pero ¿cómo podría saberlo? ( Actualización : String#chars no fue el culpable - vea el punto de referencia para mainly_execute_a_trivial_block )

Edición: puntos de referencia actuales usando 1.9.2 patchlevel 180:

require ''pp'' require ''benchmark'' def create_genome "gattaca" * 100 end ZILLION = 100000 def count_frequency_using_count(sequence) ZILLION.times do ["a", "c", "g", "t"].map{|letter| sequence.count(letter)} end end def count_frequency_using_chars(sequence) ZILLION.times do sequence.chars.group_by{|x| x}.map{|letter, array| [letter, array.count]} end end def count_frequency_using_inject_hash(sequence) ZILLION.times do sequence.chars.inject(Hash.new(0)) { |h, e| h[e] += 1 ; h } end end def count_frequency_using_each_with_object(sequence) ZILLION.times do sequence.chars.each_with_object(Hash.new(0)) { |char, hash| hash[char] += 1} end end def just_group_by(sequence) ZILLION.times do sequence.chars.group_by{|x| x} end end def just_chars_and_trivial_block(sequence) ZILLION.times do sequence.chars() {} end end def mainly_execute_a_trivial_block(sequence) ZILLION.times do sequence.length.times() {} end end def execute_an_empty_loop_instead(sequence) ZILLION.times do i = 0 max = sequence.length until i == max i += 1 end end end sequence = create_genome puts RUBY_VERSION Benchmark.bm do |benchmark| benchmark.report do count_frequency_using_count(sequence) end benchmark.report do count_frequency_using_chars(sequence) end benchmark.report do count_frequency_using_inject_hash(sequence) end benchmark.report do count_frequency_using_each_with_object(sequence) end benchmark.report do just_group_by(sequence) end benchmark.report do just_chars_and_trivial_block(sequence) end benchmark.report do mainly_execute_a_trivial_block(sequence) end benchmark.report do execute_an_empty_for_loop_instead(sequence) end end

Resultados:

user system total real 0.510000 0.000000 0.510000 ( 0.508499) # count_frequency_using_count 23.470000 0.030000 23.500000 ( 23.575487) # count_frequency_using_chars 32.770000 0.050000 32.820000 ( 32.874634) # count_frequency_using_inject_hash 31.880000 0.040000 31.920000 ( 31.942437) # count_frequency_using_each_with_object 22.950000 0.030000 22.980000 ( 22.970500) # just_group_by 13.300000 0.020000 13.320000 ( 13.314466) # just_chars_and_trivial_block 5.660000 0.000000 5.660000 ( 5.661516) # mainly_execute_a_trivial_block 1.930000 0.010000 1.940000 ( 1.934861) # execute_an_empty_loop_instead


Una manera más rápida y más lenta sería pasar la matriz solo una vez.

hash = sequence.chars.inject(Hash.new(0)) { |h, e| h[e] += 1 ; h } => {"g"=>100, "a"=>300, "t"=>200, "c"=>100}

pero en realidad NO es más rápido

Esto fue un fracaso total de copiar y pegar.

De todas formas, dejo la respuesta ya que muestra cómo se usa Benchmark en la biblioteca estándar.

require ''pp'' require ''benchmark'' def count_frequency_using_inject_hash(sequence) 100000.times do sequence.chars.inject(Hash.new(0)) { |h, e| h[e] += 1 ; h } end end sequence = create_genome Benchmark.bm do |benchmark| benchmark.report do pp count_frequency_using_chars(sequence) end benchmark.report do pp count_frequency_using_count(sequence) end benchmark.report do pp count_frequency_using_inject_hash(sequence) end end

user system total real 41.500000 0.000000 41.500000 ( 42.484375) 1.312000 0.000000 1.312000 ( 1.406250) 49.265000 0.000000 49.265000 ( 49.348633)


No tiene nada que ver con el rubí interno. Estás comparando manzanas con naranjas.

en su primer ejemplo, está agrupando 700 cadenas de caracteres 100000 veces y buscando el recuento. Entonces es un problema en tu lógica. no en contar. En el segundo enfoque solo estás contando,

Y en ambos enfoques, solo estás usando recuento

solo cambia el primer ejemplo como este

def count_frequency_using_chars(sequence) grouped = sequence.chars.group_by{|x| x} 100000.times do grouped.map{|letter, array| [letter, array.count]} end end

Y es tan rápido como tu segundo

Editar

Este enfoque es 3 veces más rápido que count_frequency_using_count , verifica los puntos de referencia

def count_frequency_using_chars_with_single_group(sequence) grouped = sequence.chars.group_by{|x| x} 100000.times do grouped.map{|letter, array| [letter, array.count]} end end def count_frequency_using_count(sequence) 100000.times do ["a", "c", "g", "t"].map{|letter| sequence.count(letter)} end end Benchmark.bm do |benchmark| benchmark.report do pp count_frequency_using_chars_with_single_group(sequence) end benchmark.report do pp count_frequency_using_count(sequence) end end user system total real 0.410000 0.000000 0.410000 ( 0.419100) 1.330000 0.000000 1.330000 ( 1.324431)

Andrew a tus comentarios,

measuring the character composition of 100000 sequences once each, not the character composition of one sequence 100000 times , aún así su enfoque de recuento es demasiado lento que el enfoque de group_by. Acabo de comparar las grandes cadenas como dijiste

seq = "gattaca" * 10000 #seq length is 70000 arr_seq = (1..10).map {|x| seq} #10 seq items

y cambió los métodos para manejar las múltiples secuencias

def count_frequency_using_chars_with_single_group(sequences) sequences.each do |sequence| grouped = sequence.chars.group_by{|x| x} 100000.times do grouped.map{|letter, array| [letter, array.count]} end end end def count_frequency_using_count(sequence) sequences.each do |sequence| 100000.times do ["a", "c", "g", "t"].map{|letter| sequence.count(letter)} end end end Benchmark.bm do |benchmark| benchmark.report do pp count_frequency_using_chars_with_single_group(arr_seq) end benchmark.report do pp count_frequency_using_count(arr_seq) end end

Para procesar 100000 veces, 10 secuencias cada una con 70000 de longitud

user system total real 3.710000 0.040000 3.750000 ( 23.452889) #modified group_by approach 1039.180000 6.920000 1046.100000 (1071.374730) #simple char count approach

Su enfoque simple de recuento de caracteres es un 47% más lento que el enfoque group_by modificado para las cadenas de alto volumen. Ejecuté el punto de referencia anterior para solo 10 secuencias cada una con 70000 de longitud. Supongamos esto para 100 o 1000 secuencias, el conteo simple nunca sería una opción. ¿derecho?


Puede ver mejor lo que está sucediendo al crear un perfil de la máquina virtual.

Un bloqueo que se produce un número excesivo de veces es el culpable aquí. Si usa el perfilador de Perftools para la máquina virtual, siga las instrucciones enumeradas en "Creación de perfiles de las extensiones de Ruby VM y C" en https://github.com/tmm1/perftools.rb (nota: esto es más o menos perftools vainilla, no perftools.rb)

Removing _init from all stack traces. Total: 3883 samples 1321 34.0% 34.0% 3883 100.0% rb_yield_0 273 7.0% 41.1% 274 7.1% _IO_str_pbackfail 191 4.9% 46.0% 191 4.9% __i686.get_pc_thunk.bx 171 4.4% 50.4% 171 4.4% _init 131 3.4% 53.7% 3880 99.9% rb_eval 122 3.1% 56.9% 347 8.9% st_lookup 105 2.7% 59.6% 423 10.9% new_dvar 93 2.4% 62.0% 326 8.4% rb_newobj 89 2.3% 64.3% 89 2.3% _setjmp 77 2.0% 66.3% 400 10.3% str_new 67 1.7% 68.0% 357 9.2% dvar_asgn_internal 63 1.6% 69.6% 204 5.3% malloc 62 1.6% 71.2% 3820 98.4% rb_str_each_char 58 1.5% 72.7% 187 4.8% rb_ary_store 55 1.4% 74.1% 55 1.4% rb_memcmp 55 1.4% 75.5% 3883 100.0% rb_yield # rest snipped for brevity

Como puede ver, rb_yield_0 representa más de un tercio de la actividad, por lo que incluso si pudiera optimizar todo lo demás, aún sería más lento que si estuviera usando String#count .

También puede confirmar esto haciendo un punto de referencia donde solo está creando un bloque que no hace nada.

require ''pp'' require ''benchmark'' def create_genome "gattaca" * 100 end ZILLION = 100000 def mainly_execute_a_trivial_block(sequence) ZILLION.times do sequence.length.times() {} end end def execute_an_empty_loop_instead(sequence) ZILLION.times do i = 0 max = sequence.length until i == max i += 1 end end end sequence = create_genome puts RUBY_VERSION Benchmark.bm do |benchmark| benchmark.report do pp mainly_execute_a_trivial_block(sequence) end benchmark.report do pp execute_an_empty_loop_instead(sequence) end end

da

user system total real 5.700000 0.000000 5.700000 ( 5.727715) # mainly_execute_a_trivial_block 1.920000 0.000000 1.920000 ( 1.942096) # execute_an_empty_loop_instead