ideasman42
El desenrollado de bucles es una optimización común, pero ¿también se hace lo contrario?
(para reducir el tamaño de la salida del archivo objeto, un binario más pequeño).
Tengo curiosidad por saber si es una técnica común para que los compiladores eliminen bloques de código idénticos y sucesivos (o llamadas a funciones) en un bucle, o extraigan un bloque duplicado en una función estática.
Estoy interesado porque hay bibliotecas de solo encabezado.*
en C, que puede agregar una gran cantidad de código duplicado, por lo que sería útil saber si algunos compiladores de C pueden detectar esto y manejarlo de manera más eficiente.
*
Por biblioteca de solo encabezado, me refiero a un encabezado que define el código directamente en lugar de definiciones de funciones.
Y si esto se hace, sería útil saber bajo qué condiciones y restricciones se aplican para garantizar que se pueda utilizar.
Nota (Para el propósito de la pregunta, cualquier compilador C popular está bien GCC/Clang/Intel/MSVC).
Una biblioteca de solo encabezado que encontré, llamada utash usa algunas macros muy grandes, y quería saber si había algún truco del compilador que pudiera desduplicar inteligentemente bloques de código tan grandes, ver: por ejemplo uthash.hotro ejemplo similar es qsort.h en línea
Ejemplo de un bloque que podría ser deduplicado (resulta Py_DECREF
puede expandirse en un bloque de código bastante grande).
#define PY_ADD_TO_DICT(dict, i) \
do {
PyDict_SetItemString(dict, names[i], item = PyUnicode_FromString(values[i])); \
Py_DECREF(item); \
} while (0)
/* this could be made into a loop */
PY_ADD_TO_DICT(d, 0);
PY_ADD_TO_DICT(d, 1);
PY_ADD_TO_DICT(d, 2);
PY_ADD_TO_DICT(d, 3);
Tenga en cuenta que esto es artificial, pero se basa en un ejemplo real.
Aclaración
Parece que la respuesta corta a mi pregunta es No (o solo en algunos casos limitados/triviales), solo para aclarar por qué estaba preguntando un poco más.
Algunas respuestas en los comentarios parecen asumir que cualquier persona simplemente refactorizaría el código en una función.
Esto es casi siempre la mejor opción por supuesto, sin embargo hay ocasiones en las que pueden aparecer bloques de código muy similar.
- código de placa de caldera creado por un generador de código no muy inteligente.
- cuando se usa una API externa que expone parte de su funcionalidad como macros (en este caso, la envoltura local en funciones funciona en la mayoría de los casos, por supuesto, pero esto significa que su código tiene sus propias peculiaridades que no son típicas para los usos de esas API).
- cuando no puede reemplazar macros con funciones, hay algunos casos raros en los que termina siendo poco práctico hacer esto.
- al importar código desde una base de código externa, no siempre es ideal ingresar y comenzar a limpiar su código; al evaluar esa base de código, es útil tener una idea de qué tan inteligente será el compilador para optimizar el código.
En todos estos casos es posible deduplicar (generar código más inteligente, envolver macros en funciones, parchear bibliotecas de terceros)pero antes de hacer un esfuerzo para hacer tales cosas, vale la pena saber cuánto trabajo está haciendo el compilador por nosotros.
Retroceso1986
Dependiendo de la cadena de herramientas, puede tener opciones para entrenar al compilador y al enlazador para que reconozcan y fusionen código redundante. Algunas buenas palabras clave de Google incluyen:
- “factorización de código” y palabras clave adicionales
- “optimización de todo el programa”
- “optimización interprocedimiento” Wikipedia
- “optimización del tiempo de enlace” LTO
Tenga en cuenta que el página de optimizaciones gcc mencionado en comentarios anteriores proporciona algunas banderas de interés, a saber:
- -ftree-cola-fusión
- -ftree-switch-conversion
- -fgcse
- -f salto cruzado
- -fipa-pta
- -fipa-icf (plegado de código idéntico), agregado en GCC5.x
- -fopa-cp
- -flto
- -fprograma completo
Finalmente, estas publicaciones de blog son informativas:
Lo que podría ser la forma más fácil de describir esto se conoce como código de refactorización. Este es el tipo de cosas que podría hacer a mano como desarrollador, pero esta no es una característica de los compiladores y optimizadores de C modernos, al menos en lo que respecta a GCC. A continuación se incluye una lista de todas las optimizaciones individuales que puede configurar (a través de -O0
y -O2
respectivamente) en GCC, por ejemplo, y ninguno de ellos refactoriza una serie de declaraciones similares para usar un índice común en un bucle.
Optimization Level Zero Optimization Level Two
============================================================================================================
-fauto-inc-dec -fthread-jumps
-fbranch-count-reg -falign-functions -falign-jumps
-fcombine-stack-adjustments -falign-loops -falign-labels
-fcompare-elim -fcaller-saves
-fcprop-registers -fcrossjumping
-fdce -fcse-follow-jumps -fcse-skip-blocks
-fdefer-pop -fdelete-null-pointer-checks
-fdelayed-branch -fdevirtualize -fdevirtualize-speculatively
-fdse -fexpensive-optimizations
-fforward-propagate -fgcse -fgcse-lm
-fguess-branch-probability -fhoist-adjacent-loads
-fif-conversion2 -finline-small-functions
-fif-conversion -findirect-inlining
-finline-functions-called-once -fipa-cp
-fipa-pure-const -fipa-sra
-fipa-profile -fisolate-erroneous-paths-dereference
-fipa-reference -foptimize-sibling-calls
-fmerge-constants -foptimize-strlen
-fmove-loop-invariants -fpartial-inlining
-fshrink-wrap -fpeephole2
-fsplit-wide-types -freorder-blocks -freorder-blocks-and-partition -freorder-functions
-ftree-bit-ccp -frerun-cse-after-loop
-ftree-ccp -fsched-interblock -fsched-spec
-fssa-phiopt -fschedule-insns -fschedule-insns2
-ftree-ch -fstrict-aliasing -fstrict-overflow
-ftree-copy-prop -ftree-builtin-call-dce
-ftree-copyrename -ftree-switch-conversion -ftree-tail-merge
-ftree-dce -ftree-pre
-ftree-dominator-opts -ftree-vrp
-ftree-dse -fuse-caller-save
-ftree-forwprop
-ftree-fre
-ftree-phiprop
-ftree-sink
-ftree-slsr
-ftree-sra
-ftree-pta
-ftree-ter
-funit-at-a-time
Referencias
- 3.10 – Opciones que controlan la optimizaciónAcceso 2014-07-07,
<https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html>
-
Sin embargo, a menudo desearía que existiera esta característica. También para interruptores. Encuentro que el código de mi empresa tiene muchos interruptores donde el cuerpo de cada
case
es idéntico excepto por algún valor. Estos deberían haber sido una tabla, pero no creo que el compilador sea lo suficientemente inteligente como para darse cuenta de eso.– Pato mugido
08/08/2014 a las 18:05
-
@MooingDuck Hace que me pregunte por qué necesitan un
switch
en lugar de un soloif-else
.– Nube
9 de agosto de 2014 a las 12:58
-
porque estos bloques pueden tener literalmente miles de declaraciones de casos: coliru.stacked-crooked.com/a/ff4e7a32e6696c2c
– Pato mugido
11 de agosto de 2014 a las 16:09
Roble
Aunque parece muy factible, nunca he visto ninguna optimización que tome dos piezas de código similares y las combine en un solo ciclo.
Sin embargo, hay dos casos de duplicación que son eliminado por el compilador (más obviamente, porque también hay una ganancia de rendimiento, no solo una ganancia de tamaño de código):
-
Cuando dos expresiones se evalúan al mismo valor, en la misma función, una técnica llamada eliminación de subexpresiones comunes elimina los cálculos redundantes.
Curiosamente, con la inserción agresiva y la optimización del tiempo de enlace, esto podría cubrir cada vez más casos, pero, por supuesto, la inserción tiene su propio costo de tamaño binario severo. -
Cuando es posible vectorizar el código, un compilador de vectorización a menudo puede identificar eso y hacer la vectorización por usted.
Aparte de eso, deberá refactorizar manualmente el código para eliminar la duplicación.
-
¿No quisiste decir “refactorizar el código para eliminar duplicación.” en la última oración?
– glamuroso
09/08/2014 a las 21:55
“Tengo curiosidad por saber si es una técnica común para que los compiladores eliminen el código duplicado, en un bucle”. No entiendo lo que quieres decir con “desduplicar el código en un bucle”. ¿Está preguntando si el compilador tomaría múltiples declaraciones similares, asignaría una matriz, insertaría todas las variables del mismo tipo pero diferentes e iteraría sobre esa matriz? No… no lo hará. ¿Por qué lo haría? ¿Qué considera exactamente “código duplicado”? Las restricciones en el compilador están definidas por el estándar.
– Ed S.
8 de agosto de 2014 a las 1:25
Creo que está preguntando sobre una generalización de la eliminación de subexpresiones comunes, pero aplicándola a declaraciones completas o bloques de código.
– Barmar
8 de agosto de 2014 a las 1:33
@ ideasman42 Si el programador pensó que sería mejor como una función, ¿por qué no simplemente escribieron una función y la macro la llamó?
– Barmar
8 de agosto de 2014 a las 1:33
El compilador ni siquiera ve macros. La expansión de macros es el trabajo del preprocesador. ¿Quizás podrías agregar un ejemplo?
– Ed S.
8 de agosto de 2014 a las 1:35
Dudo que la mayoría de los compiladores pasen mucho tiempo haciendo esto. Es mucho mejor dejar el código como está y eliminar las subexpresiones comunes.
– Lame caliente
8 de agosto de 2014 a las 2:02