¿Por qué abrir un archivo más rápido que leer contenido variable?

36

En un bashscript necesito varios valores de /proc/archivos. Hasta ahora tengo docenas de líneas agrupando los archivos directamente así:

grep -oP '^MemFree: *\K[0-9]+' /proc/meminfo

En un esfuerzo por hacerlo más eficiente, guardé el contenido del archivo en una variable y seleccioné eso:

a=$(</proc/meminfo)
echo "$a" | grep -oP '^MemFree: *\K[0-9]+'

En lugar de abrir el archivo varias veces, esto debería abrirlo una sola vez y seleccionar el contenido variable, que supuse sería más rápido, pero de hecho es más lento:

bash 4.4.19 $ time for i in {1..1000};do grep ^MemFree /proc/meminfo;done >/dev/null
real    0m0.803s
user    0m0.619s
sys     0m0.232s
bash 4.4.19 $ a=$(</proc/meminfo)
bash 4.4.19 $ time for i in {1..1000};do echo "$a"|grep ^MemFree; done >/dev/null
real    0m1.182s
user    0m1.425s
sys     0m0.506s

Lo mismo es cierto para dashy zsh. Sospeché el estado especial de los /proc/archivos como una razón, pero cuando copio el contenido de /proc/meminfoun archivo normal y uso que los resultados son los mismos:

bash 4.4.19 $ cat </proc/meminfo >meminfo
bash 4.4.19 $ time for i in $(seq 1 1000);do grep ^MemFree meminfo; done >/dev/null
real    0m0.790s
user    0m0.608s
sys     0m0.227s

El uso de una cadena here para guardar la tubería lo hace un poco más rápido, pero aún no tan rápido como con los archivos:

bash 4.4.19 $ time for i in $(seq 1 1000);do <<<"$a" grep ^MemFree; done >/dev/null
real    0m0.977s
user    0m0.758s
sys     0m0.268s

¿Por qué abrir un archivo más rápido que leer el mismo contenido de una variable?

postre
fuente
@ l0b0 Esta suposición no es defectuosa, la pregunta muestra cómo se me ocurrió y las respuestas explican por qué es así. Su edición ahora hace que las respuestas ya no respondan a la pregunta del título: no dicen si ese es el caso.
postre
OK, aclarado Debido a que el encabezado era incorrecto en la gran mayoría de los casos, simplemente no para ciertos archivos especiales mapeados en memoria.
l0b0
@ l0b0 No, eso es lo que estoy pidiendo aquí: “Yo sospechaba el estado especial de /proc/archivos como una razón, pero cuando copio el contenido de /proc/meminfoun archivo normal y el uso que los resultados son los mismos:” Es no especial /proc/archivos, ¡leer archivos normales también es más rápido!
postre

Respuestas:

47

Aquí, no se trata de abrir un archivo en lugar de leer el contenido de una variable, sino más bien de bifurcar un proceso adicional o no.

grep -oP '^MemFree: *\K[0-9]+' /proc/meminfobifurca un proceso que se ejecuta y grepque se abre /proc/meminfo(un archivo virtual, en la memoria, sin E / S de disco involucrado) lo lee y coincide con la expresión regular.

La parte más costosa es bifurcar el proceso y cargar la utilidad grep y sus dependencias de la biblioteca, hacer la vinculación dinámica, abrir la base de datos de configuración regional, docenas de archivos que están en el disco (pero probablemente en caché en la memoria).

La parte sobre leer /proc/meminfo es insignificante en comparación, el núcleo necesita poco tiempo para generar la información allí y grepnecesita poco tiempo para leerlo.

Si se ejecuta strace -cen eso, verá que las llamadas a los sistemas uno open()y uno read()utilizados para leer /proc/meminfoson cacahuetes en comparación con todo lo demás.grep hace para comenzar ( strace -cno cuenta la bifurcación).

En:

a=$(</proc/meminfo)

En la mayoría de los depósitos que soportan eso $(<...) operador ksh, el shell simplemente abre el archivo y lee su contenido (y elimina los caracteres de la nueva línea final). bashes diferente y mucho menos eficiente, ya que bifurca un proceso para hacer esa lectura y pasa los datos al padre a través de una tubería. Pero aquí, se hace una vez, así que no importa.

En:

printf '%s\n' "$a" | grep '^MemFree'

El caparazón necesita generar dos procesos, que se ejecutan simultáneamente pero interactúan entre sí a través de una tubería. Esa creación de tubería, derribar y escribir y leer tiene un costo pequeño. El costo mucho mayor es el engendro de un proceso adicional. La programación de los procesos también tiene algún impacto.

Puede encontrar que usar el <<<operador zsh lo hace un poco más rápido:

grep '^MemFree' <<< "$a"

En zsh y bash, eso se hace escribiendo el contenido de $aun archivo temporal, que es menos costoso que generar un proceso adicional, pero probablemente no le dará ninguna ganancia en comparación con obtener los datos directamente /proc/meminfo. Eso sigue siendo menos eficiente que su enfoque que copia /proc/meminfoen el disco, ya que la escritura del archivo temporal se realiza en cada iteración.

dashno admite cadenas aquí, pero sus documentos heredados se implementan con una tubería que no implica generar un proceso adicional. En:

 grep '^MemFree' << EOF
 $a
 EOF

El caparazón crea una tubería, bifurca un proceso. El niño se ejecuta grepcon su stdin como el extremo de lectura de la tubería, y el padre escribe el contenido en el otro extremo de la tubería.

Pero es probable que el manejo de la tubería y la sincronización del proceso sigan siendo más costosos que simplemente obtener los datos directamente /proc/meminfo .

El contenido de /proc/meminfoes corto y no lleva mucho tiempo producirlo. Si desea guardar algunos ciclos de CPU, desea eliminar las partes costosas: bifurcar procesos y ejecutar comandos externos.

Me gusta:

IFS= read -rd '' meminfo < /proc/meminfo
memfree=${meminfo#*MemFree:}
memfree=${memfree%%$'\n'*}
memfree=${memfree#"${memfree%%[! ]*}"}

Sin bashembargo, evite cuyo patrón de coincidencia es muy ineficiente. Con zsh -o extendedglob, puedes acortarlo a:

memfree=${${"$(</proc/meminfo)"##*MemFree: #}%%$'\n'*}

Tenga en cuenta que ^es especial en muchos shells (Bourne, fish, rc, es y zsh con la opción de globo extendido al menos), recomendaría citarlo. También tenga en cuenta que echono se puede utilizar para generar datos arbitrarios (de ahí mi uso de lo printfanterior).

Stéphane Chazelas
fuente
44
En el caso que printfusted dice que el shell necesita generar dos procesos, pero ¿no es printfun shell incorporado?
David Conrad
66
@DavidConrad Lo es, pero la mayoría de los shells no intentan analizar la tubería para las partes que podría ejecutar en el proceso actual. Simplemente se bifurca y deja que los niños lo descubran. En este caso, el proceso padre se bifurca dos veces; el niño para el lado izquierdo luego ve un incorporado y lo ejecuta; el niño del lado derecho ve grepy ejecuta.
Chepner
1
@DavidConrad, la tubería es un mecanismo de IPC, por lo que, en cualquier caso, las dos partes tendrán que ejecutarse en diferentes procesos. Mientras que A | B, hay algunos shells como AT&T ksh o zsh que se ejecutan Ben el proceso de shell actual si se trata de un comando incorporado o compuesto o de función, no conozco ninguno que se ejecute Aen el proceso actual. En todo caso, para hacer eso, tendrían que manejar SIGPIPE de una manera compleja como si se Aestuviera ejecutando en un proceso secundario y sin terminar el shell para que el comportamiento no sea demasiado sorprendente cuando Bsale temprano. Es mucho más fácil ejecutar Bel proceso padre.
Stéphane Chazelas
Bash apoya<<<
D. Ben Knoble
1
@ D.BenKnoble, no quise decir que bashno era compatible <<<, solo que el operador vino zshcomo $(<...)vino de ksh.
Stéphane Chazelas
6

En su primer caso, solo está utilizando la utilidad grep y está buscando algo del archivo /proc/meminfo, /proces un sistema de archivos virtual, por lo que el /proc/meminfoarchivo está en la memoria y requiere muy poco tiempo para recuperar su contenido.

Pero en el segundo caso, está creando una tubería, luego pasa la salida del primer comando al segundo comando usando esta tubería, lo cual es costoso.

La diferencia se debe a /proc(porque está en la memoria) y a la tubería, consulte el siguiente ejemplo:

time for i in {1..1000};do grep ^MemFree /proc/meminfo;done >/dev/null

real    0m0.914s
user    0m0.032s
sys     0m0.148s


cat /proc/meminfo > file
time for i in {1..1000};do grep ^MemFree file;done >/dev/null

real    0m0.938s
user    0m0.032s
sys     0m0.152s


time for i in {1..1000};do echo "$a"|grep ^MemFree; done >/dev/null

real    0m1.016s
user    0m0.040s
sys     0m0.232s
Prvt_Yadav
fuente
1

Está llamando a un comando externo en ambos casos (grep). La llamada externa requiere una subshell. Bifurcar ese caparazón es la causa fundamental de la demora. Ambos casos son similares, por lo tanto: un retraso similar.

Si desea leer el archivo externo solo una vez y usarlo (desde una variable) varias veces, no salga del shell:

meminfo=$(< /dev/meminfo)    
time for i in {1..1000};do 
    [[ $meminfo =~ MemFree:\ *([0-9]*)\ *.B ]] 
    printf '%s\n' "${BASH_REMATCH[1]}"
done

Lo que toma solo alrededor de 0.1 segundos en lugar del 1 segundo completo para la llamada grep.

Isaac
fuente