C/C++: Calificador restrict
Hay que ver lo que son las cosas. Leyéndome un libro de OpenMP descubro una característica de C/C++ bastante importante que había pasado inadvertida para mi durante todos estos años que llevo programando. Resulta que el calificador restrict (o __restrict__ en C++) permite especificar si sabemos a ciencia cierta si el puntero al que añadimos este calificador apunta a un objeto que no es apuntado por ningún otro puntero. Esto es especialmente útil para programas científicos donde los cálculos realizados sobre matrices abundan especialmente. Lo realmente interesante es que al usar este calificador el compilador puede introducir algunas mejoras en el código objeto resultante que mejorarán la eficiencia, y por tanto el tiempo de computo, de nuestros programas. A continuación os muestro unos ejemplos sencillos de como usar el calificador y los tiempos de ejecución obtenidos.
El calificador restrict
El calificador restrict trata el problema de que los solapamientos en memoria pueden inhibir optimizaciones. Específicamente, si un compilador no puede determinar que dos punteros diferentes estén siendo usados para referenciar distintos objetos, entonces este no puede aplicar optimizaciones tales como la disposición de los valores de dichos objetos en registros en vez de en memoria, o el re-ordenamiento de las cargas y almacenamientos de dichos valores.
El calificador restrict fue diseñado para expresar y extender dos tipos de solapamiento (aliasing) de la información ya especificados en el lenguaje:
- Si un puntero simple es directamente asignado al valor de retorno de una invocación de malloc, entonces dicho puntero es el único medio de acceso al objeto reservado (es decir, otro puntero puede tener acceso a dicho puntero solo cuando se le asigna una valor que está basado en el valor del primer puntero). Declarando el puntero con el calificador restrict expresa dicha información al compilador. Además, el calificador puede ser usado para extender un tratamiento especial del compilador de dicho puntero a situaciones más generales. Por ejemplo, una invocación de malloc puede estar oculta al compilador en otra función, o una sola invocación de malloc puede ser usada para reservar varios objetos, cada uno referenciado a través de su propio puntero.
- La librería especifica dos versiones de una función de copia de objeto, ya que en muchos sistemas es posible una copia más rápida si se sabe que los arrays de origen y de destino no se solapan. El calificador restrict se puede usar para expresar la restricción de solapamiento en un nuevo prototipo que es compatible con la versión original:
void *memcpy(void * restrict s1, const void * restrict s2, size_t n); void *memmove(void * s1, const void * s2, size_t n);
Siendo la restricción visible al compilador, una implementación más sencilla de memcpy en C da un nivel de rendimiento similar al anteriormente requerido por el lenguaje ensamblador. Por lo tanto el calificador restrict proporciona un medio estandar con el que hacer, en la definición de cualquier función, una restricción de solapamiento de un tipo.
A continuación os dejo un ejemplo con el que podréis ver la asombrosa diferencia de rendimiento conseguida:
// ===================================================================
//
// Filename: restrict.cpp
//
// Description:
//
// Version: 1.0
// Created: 13/03/10 12:14:51
// Revision: none
// Compiler: g++
//
// Author: piponazo, piponazo@plagatux.com
// Company: http://plagatux.es
//
// ===================================================================
#include <iostream>
#include <cstdlib>
#include <gu/gutimemark.h>
#include <sys/timeb.h>
using namespace std;
void f_restrict (int n, int o, float * __restrict__ a1, const float * __restrict__ a2)
{
int i;
for (int j=0; j<o; j++)
for (i=0; i<n; i++)
a1[i] *= (a1[i] + a2[i] * a2[i]);
}
void f_no_restrict (int n, int o, float *a1, const float *a2)
{
int i;
for (int j=0; j<o; j++)
for (int i=0; i<n; i++)
a1[i] *= (a1[i] + a2[i] * a2[i]);
}
int main()
{
struct timeb startT, finishT;
unsigned int seconds, milliseconds;
int nelements=100000, operations=100;
float *a = new float[nelements];
float *b = new float[nelements];
for (int i=0; i<nelements; i++)
{
a[i] = 1.0;
b[i] = rand();
}
ftime(&startT);
f_restrict(nelements, operations, a, b);
ftime(&finishT);
seconds = finishT.time - startT.time - 1;
milliseconds = (1000 - startT.millitm) + finishT.millitm;
cout << "Time of execution: " << (milliseconds + seconds * 1000) << endl;
for (int i=0; i<nelements; i++)
a[i] = 1.0;
ftime(&startT);
f_no_restrict(nelements, operations, a, b);
ftime(&finishT);
seconds = finishT.time - startT.time - 1;
milliseconds = (1000 - startT.millitm) + finishT.millitm;
cout << "Time of execution: " << (milliseconds + seconds * 1000) << endl;
delete [] a;
delete [] b;
}
La ejecución del programa nos da como resultado:
Time of execution: 15 Time of execution: 1914
En milisegundos.
It has to be seen to be believed. Reading a OpenMP book I have found a very important feature of C/C++ that had gone unnoticed for me since I’m a programmer. The restrict qualifier (or __restrict__ in C++) allow us specify if we know for sure whether the pointer at which the qualifier is added points to an object not pointed by other pointers. This is specially useful for scientist programs where there is plentiful calculations over object, e.g, vectors or matrices. What is really interesting is that when using this qualifier the compiler can introduce some improvements in the resultant object code that improve the performance in our programs. In the following, I show you some simple examples of how to use this qualifier and the computational times obtained.
The restrict qualifier
All the information obtained in the spanish version of this post has been obtained from these web resources:
- How to Use the restrict Qualifier in C.
- http://publib.boulder.ibm.com/infocenter/comphelp/v7v91/index.jsp?topic=/com.ibm.vacpp7a.doc/language/ref/clrc03restrict_type_qualifier.htm
However, the example of code shown in the spanish section has been written by me
.
Running the program we obtain:
Time of execution: 15 Time of execution: 1914
In milliseconds.
loading...




loading...
Very interesting. Thanks!
loading...
Muy buena explicación, me parece muy interesante el modificador __restric__.
Lo realmente interesante puede residir en conocer alguna función que detecte operaciones sustancialmente mejorables con esta técnica y que las aplique por defecto donde se consiga mejor rendimiento, ganando así que no tenga que hacerse explícito por el programador.
loading...
La verdad que no estaría nada mal tener algo como lo que me comentas, pero me temo mucho que es imposible hacer algo así para cualquier código genérico, sino ya existiría seguramente
. Lo curioso es que es una característica de C/C++ que nunca había visto, y descubrirla por casualidad leyendome un libro sobre otro tema me ha abierto un nuevo abanico de posibilidades para mejorar mis programas
.
Saludos.