¿Uso realista de la palabra clave 'restringir' C99?

183

Estaba buscando documentación y preguntas / respuestas y la vi mencionada. Leí una breve descripción, afirmando que sería básicamente una promesa del programador que el puntero no se usará para apuntar a otro lado.

¿Alguien puede ofrecer algunos casos realistas en los que valga la pena usar esto?

user90052
fuente
44
memcpyvs memmovees un ejemplo canónico.
Alexandre C.
@AlexandreC .: No creo que sea particularmente aplicable, ya que la falta de un calificador "restringido" no implica que la lógica del programa funcionará con la sobrecarga de la fuente y el destino, ni la presencia de dicho calificador evitaría que un método llamado determinar si la fuente y el destino se superponen y, si es así, reemplazar dest con src + (dest-src) que, dado que se deriva de src, se le permitiría alias.
supercat
@supercat: Por eso lo puse como comentario. Sin embargo, 1) restrict-calificar argumentos para memcpypermitir en principio una implementación ingenua para ser optimizada agresivamente, y 2) simplemente llamar memcpypermite al compilador suponer que los argumentos que se le dan no tienen alias, lo que podría permitir cierta optimización en torno a la memcpyllamada.
Alexandre C.
@AlexandreC .: Sería muy difícil para un compilador en la mayoría de las plataformas optimizar una memoria ingenua, incluso con "restricción", para ser tan eficiente como una versión adaptada al objetivo. Las optimizaciones del lado de la llamada no requerirían la palabra clave "restringir", y en algunos casos los esfuerzos para facilitarlas pueden ser contraproducentes. Por ejemplo, muchas implementaciones de memcpy podrían, sin costo adicional, considerarse memcpy(anything, anything, 0);como un no-op, y asegurar que si pes un puntero a al menos nbytes de escritura memcpy(p,p,n),; no tendrá efectos secundarios adversos. Tales casos pueden surgir ...
supercat
... naturalmente en ciertos tipos de código de aplicación (por ejemplo, una rutina de clasificación que intercambia un elemento consigo mismo), y en implementaciones donde no tienen efectos secundarios adversos, permitir que esos casos sean manejados por el código de caso general puede ser más eficiente que tener para agregar pruebas de casos especiales. Desafortunadamente, algunos escritores de compiladores parecen pensar que es mejor exigir que los programadores agreguen código que el compilador no pueda optimizar, para facilitar las "oportunidades de optimización" que los compiladores rara vez explotarían de todos modos.
supercat

Respuestas:

182

restrictdice que el puntero es lo único que accede al objeto subyacente. Elimina el potencial de alias de puntero, permitiendo una mejor optimización por parte del compilador.

Por ejemplo, supongamos que tengo una máquina con instrucciones especializadas que pueden multiplicar vectores de números en la memoria, y tengo el siguiente código:

void MultiplyArrays(int* dest, int* src1, int* src2, int n)
{
    for(int i = 0; i < n; i++)
    {
        dest[i] = src1[i]*src2[i];
    }
}

El compilador debe manejar adecuadamente if dest, src1y src2superponerse, lo que significa que debe hacer una multiplicación a la vez, de principio a fin. Al tener restrict, el compilador es libre de optimizar este código utilizando las instrucciones vectoriales.

Wikipedia tiene una entrada restrict, con otro ejemplo, aquí .

Miguel
fuente
3
@Michael: si no me equivoco, el problema sería solo cuando destse superponga cualquiera de los vectores de origen. ¿Por qué habría un problema si src1y src2se superponen?
ysap
1
restringir normalmente tiene un efecto solo cuando se apunta a un objeto que se modifica, en cuyo caso afirma que no es necesario tener en cuenta los efectos secundarios ocultos. La mayoría de los compiladores lo usan para facilitar la vectorización. Msvc utiliza la verificación del tiempo de ejecución para la superposición de datos para ese propósito.
tim18
Agregar la palabra clave de registro a la variable de bucle for también lo hace más rápido además de agregar restricción.
2
En realidad, la palabra clave de registro es solo de asesoramiento. Y en los compiladores desde aproximadamente el año 2000, la i (y la n para la comparación) en el ejemplo se optimizará en un registro, ya sea que use o no la palabra clave de registro.
Mark Fischler
154

El ejemplo de Wikipedia es muy esclarecedor.

Muestra claramente cómo permite guardar una instrucción de ensamblaje .

Sin restricción:

void f(int *a, int *b, int *x) {
  *a += *x;
  *b += *x;
}

Pseudo ensamblaje:

load R1  *x    ; Load the value of x pointer
load R2  *a    ; Load the value of a pointer
add R2 += R1    ; Perform Addition
set R2  *a     ; Update the value of a pointer
; Similarly for b, note that x is loaded twice,
; because x may point to a (a aliased by x) thus 
; the value of x will change when the value of a
; changes.
load R1  *x
load R2  *b
add R2 += R1
set R2  *b

Con restringir:

void fr(int *restrict a, int *restrict b, int *restrict x);

Pseudo ensamblaje:

load R1  *x
load R2  *a
add R2 += R1
set R2  *a
; Note that x is not reloaded,
; because the compiler knows it is unchanged
; "load R1 ← *x" is no longer needed.
load R2  *b
add R2 += R1
set R2  *b

¿GCC realmente lo hace?

GCC 4.8 Linux x86-64:

gcc -g -std=c99 -O0 -c main.c
objdump -S main.o

Con -O0 , son lo mismo.

Con -O3:

void f(int *a, int *b, int *x) {
    *a += *x;
   0:   8b 02                   mov    (%rdx),%eax
   2:   01 07                   add    %eax,(%rdi)
    *b += *x;
   4:   8b 02                   mov    (%rdx),%eax
   6:   01 06                   add    %eax,(%rsi)  

void fr(int *restrict a, int *restrict b, int *restrict x) {
    *a += *x;
  10:   8b 02                   mov    (%rdx),%eax
  12:   01 07                   add    %eax,(%rdi)
    *b += *x;
  14:   01 06                   add    %eax,(%rsi) 

Para los no iniciados, la convención de convocatoria es:

  • rdi = primer parámetro
  • rsi = segundo parámetro
  • rdx = tercer parámetro

La salida de GCC fue incluso más clara que el artículo wiki: 4 instrucciones vs 3 instrucciones.

Matrices

Hasta ahora, tenemos ahorros en una sola instrucción, pero si el puntero representa las matrices que se van a recorrer, un caso de uso común, entonces se podrían guardar un montón de instrucciones, como menciona supercat .

Considere por ejemplo:

void f(char *restrict p1, char *restrict p2) {
    for (int i = 0; i < 50; i++) {
        p1[i] = 4;
        p2[i] = 9;
    }
}

Debido a que restrictun compilador inteligente (o humano), podría optimizar eso para:

memset(p1, 4, 50);
memset(p2, 9, 50);

que es potencialmente mucho más eficiente, ya que puede ser un ensamblaje optimizado en una implementación decente de libc (como glibc): ¿Es mejor usar std :: memcpy () o std :: copy () en términos de rendimiento?

¿GCC realmente lo hace?

GCC 5.2.1.Linux x86-64 Ubuntu 15.10:

gcc -g -std=c99 -O0 -c main.c
objdump -dr main.o

Con -O0ambos son lo mismo.

Con -O3:

  • con restricción:

    3f0:   48 85 d2                test   %rdx,%rdx
    3f3:   74 33                   je     428 <fr+0x38>
    3f5:   55                      push   %rbp
    3f6:   53                      push   %rbx
    3f7:   48 89 f5                mov    %rsi,%rbp
    3fa:   be 04 00 00 00          mov    $0x4,%esi
    3ff:   48 89 d3                mov    %rdx,%rbx
    402:   48 83 ec 08             sub    $0x8,%rsp
    406:   e8 00 00 00 00          callq  40b <fr+0x1b>
                            407: R_X86_64_PC32      memset-0x4
    40b:   48 83 c4 08             add    $0x8,%rsp
    40f:   48 89 da                mov    %rbx,%rdx
    412:   48 89 ef                mov    %rbp,%rdi
    415:   5b                      pop    %rbx
    416:   5d                      pop    %rbp
    417:   be 09 00 00 00          mov    $0x9,%esi
    41c:   e9 00 00 00 00          jmpq   421 <fr+0x31>
                            41d: R_X86_64_PC32      memset-0x4
    421:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
    428:   f3 c3                   repz retq

    Dos memsetllamadas como se esperaba.

  • sin restricción: no hay llamadas stdlib, solo un desenrollamiento de bucle ancho de 16 iteraciones que no pretendo reproducir aquí :-)

No he tenido la paciencia para compararlos, pero creo que la versión restringida será más rápida.

C99

Veamos el estándar por completo.

restrictdice que dos punteros no pueden apuntar a regiones de memoria superpuestas. El uso más común es para argumentos de función.

Esto restringe cómo se puede llamar a la función, pero permite más optimizaciones en tiempo de compilación.

Si la persona que llama no sigue el restrictcontrato, comportamiento indefinido.

El C99 N1256 borrador 6.7.3 / 7 "Calificadores de tipo" dice:

El uso previsto del calificador de restricción (como la clase de almacenamiento de registro) es para promover la optimización, y eliminar todas las instancias del calificador de todas las unidades de traducción de preprocesamiento que componen un programa conforme no cambia su significado (es decir, comportamiento observable).

y 6.7.3.1 "Definición formal de restricción" da los detalles sangrientos.

Regla de alias estricta

La restrictpalabra clave solo afecta a punteros de tipos compatibles (por ejemplo, dos int*) porque las estrictas reglas de alias dicen que el alias de tipos incompatibles es un comportamiento indefinido por defecto, por lo que los compiladores pueden asumir que no sucede y optimizar.

Ver: ¿Cuál es la estricta regla de alias?

Ver también

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fuente
9
El calificador "restringir" en realidad puede permitir ahorros mucho mayores. Por ejemplo, dado void zap(char *restrict p1, char *restrict p2) { for (int i=0; i<50; i++) { p1[i] = 4; p2[i] = 9; } }, los calificadores de restricción permitirían al compilador reescribir el código como "memset (p1,4,50); memset (p2,9,50);". La restricción es muy superior al alias basado en tipo; Es una pena que los compiladores se centren más en lo último.
supercat
@supercat gran ejemplo, agregado para responder.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
2
@ tim18: La palabra clave "restringir" puede habilitar muchas optimizaciones que incluso la optimización agresiva basada en tipos no puede. Además, la existencia de "restricción" en el lenguaje, a diferencia del alias agresivo basado en tipos, nunca hace imposible realizar tareas de la manera más eficiente posible en su ausencia (ya que el código que se rompería con "restricción" puede simplemente no lo use, mientras que el código que se rompe con TBAA agresivo a menudo debe reescribirse de manera menos eficiente).
supercat
2
@ tim18: rodea los elementos que contienen subrayados dobles en los backticks, como en __restrict. De lo contrario, los subrayados dobles pueden malinterpretarse como una indicación de que está gritando.
supercat
1
Más importante que no gritar es que los guiones bajos tienen un significado directamente relevante para el punto que está tratando de hacer.
recicla el