OpenImageIO/oiio — pull request #494 by lgritz (2013-01-24 19:18:05+00:00)
Title: Speed up half <-> float conversions

I randomly stumbled across this project: http://sourceforge.net/projects/half/
It consists of just one header file, half.hpp, and of particular note is that they use different methods than OpenEXR/IlmBase for converting between 'half' and float data types, as outlined in this whitepaper ftp://ftp.fox-toolkit.org/pub/fasthalffloatconversion.pdf

While waiting for long rendering benchmarks in my real job, I wrote some benchmarks so see how it compared to the code in OpenEXR/IlmBase.

First, I constructed this artificial benchmark:

#include
#include "half.hpp"
...

half hvals[1<<16];
float fvals[1<<16];

for (int i = 0; i < (1<<16); ++i)
((unsigned short *)&hvals)[i] = (unsigned short)i;

int hiters = 10000;
Timer t1;
for (int j = 0; j < hiters; ++j)
for (int i = 0; i < (1<<16); ++i)
fvals[i] = hvals[i];
std::cout << "OpenEXR h->f " << t1() << "\n";

Timer t2;
for (int j = 0; j < hiters; ++j)
for (int i = 0; i < (1<<16); ++i)
fvals[i] = ((half_float::half *)hvals)[i];
std::cout << "half.hpp h->f " << t2() << "\n";

Timer t3;
for (int j = 0; j < hiters; ++j)
for (int i = 0; i < (1<<16); ++i)
hvals[i] = fvals[i];
std::cout << "OpenEXR f->h " << t3() << "\n";

Timer t4;
for (int j = 0; j < hiters; ++j)
for (int i = 0; i < (1<<16); ++i)
((half_float::half *)hvals)[i] = fvals[i];
std::cout << "half.hpp f->h " << t4() << "\n";

And got the following results:

on a Linux workstation:

OpenEXR h->f 0.712912
half.hpp h->f 1.34651
OpenEXR f->h 2.79924
half.hpp f->h 1.19522

on a Macbook pro:

OpenEXR h->f 0.703952
half.hpp h->f 1.16503
OpenEXR f->h 2.58058
half.hpp f->h 0.912068

It looks like OpenEXR's method of converting half to float is faster than half.hpp (somewhere around 1.75x), but half.hpp's method of converting float to half is much faster than OpenEXR (by around 2.5x).

But upon further reflection, I realized that OpenEXR's method of h->f conversion using a single big table may be biased towards this "lots of loops" test, since after the first round the whole table would be in processor cache, whereas for a more typical (?) mixture of h->f conversions and other memory accesses, that could produce many cache misses whereas the half.hpp method of two table lookups from MUCH smaller tables might result in those tables more able to stay in cache.

So I prepared a more real-world benchmark: using imagespeed_test to read a half OpenEXR file into a float buffer, and to write float buffers to OpenEXR files. The following timings are with a 4k x 4k x 4 channel 'half' EXR file, on a Linux workstation with 12 cores:

read_image : 1.46s = 11.5 Mpel/s
read_scanline (1 at a time) : 4.06s = 4.1 Mpel/s
read_scanlines (64 at a time) : 1.94s = 8.6 Mpel/s
ImageBuf read : 1.69s = 9.9 Mpel/s
ImageCache get_pixels : 1.61s = 10.4 Mpel/s
ImageBuf read (autotile) : 2.33s = 7.2 Mpel/s
ImageCache get_pixels (autotile) : 2.28s = 7.4 Mpel/s
ImageBuf read (autotile+autoscanline) : 2.35s = 7.1 Mpel/s
ImageCache get_pixels (autotile+autoscanline): 2.26s = 7.4 Mpel/s

write_image (scanline) : 0.61s = 27.5 Mpel/s
write_image (tiled) : 0.62s = 27.0 Mpel/s
write_scanline at a time : 3.59s = 4.7 Mpel/s
write_scanlines, 64 at a time : 1.07s = 15.7 Mpel/s

And here is after a minor modification that just casts to their type (half_float::half) for the conversions, in just one or two spots in imageio.cpp:

read_image : 1.44s = 11.6 Mpel/s
read_scanline (1 at a time) : 3.97s = 4.2 Mpel/s
read_scanlines (64 at a time) : 1.95s = 8.6 Mpel/s
ImageBuf read : 1.68s = 10.0 Mpel/s
ImageCache get_pixels : 1.59s = 10.6 Mpel/s
ImageBuf read (autotile) : 2.31s = 7.3 Mpel/s
ImageCache get_pixels (autotile) : 2.23s = 7.5 Mpel/s
ImageBuf read (autotile+autoscanline) : 2.35s = 7.1 Mpel/s
ImageCache get_pixels (autotile+autoscanline): 2.19s = 7.7 Mpel/s

write_image (scanline) : 0.56s = 29.9 Mpel/s
write_image (tiled) : 0.57s = 29.3 Mpel/s
write_scanline at a time : 3.34s = 5.0 Mpel/s
write_scanlines, 64 at a time : 0.98s = 17.0 Mpel/s

So this is a more real-world test, and it seems that using the half.hpp conversion code speeds up our overall writing of OpenEXR files (for the common case of float buffer -> half file) by about 8%. The analogous reading task (read half exr -> float buffer) does appear ever so slightly faster, but only by about 1%, so not especially significant.

This patch contains the addition of half.hpp (MIT license, so compatible with us) to our project (not part of the public API, just used internally) and use for those couple of conversions, as well as the additions to imagespeed_test.cpp that add the writing speed tests.