Как оптимизировать преобразование с плавающей запятой половинной точности в поплавок одинарной точности32? - PullRequest
3 голосов
/ 01 апреля 2011

Я пытаюсь улучшить производительность своей функции. Профилировщик указывает на код во внутреннем цикле. Могу ли я улучшить производительность этого кода, возможно, используя встроенные функции SSE?

void ConvertImageFrom_R16_FLOAT_To_R32_FLOAT(char* buffer, void* convertedData, DWORD width, DWORD height, UINT rowPitch)
{
    struct SINGLE_FLOAT
    {
        union {
            struct {
                unsigned __int32 R_m : 23;
                unsigned __int32 R_e : 8;
                unsigned __int32 R_s : 1;
            };
            struct {
                float r;
            };
        };
    };
    C_ASSERT(sizeof(SINGLE_FLOAT) == 4); // 4 bytes
    struct HALF_FLOAT
    {
        unsigned __int16 R_m : 10;
        unsigned __int16 R_e : 5;
        unsigned __int16 R_s : 1;
    };
    C_ASSERT(sizeof(HALF_FLOAT) == 2);
    SINGLE_FLOAT* d = (SINGLE_FLOAT*)convertedData;
    for(DWORD j = 0; j< height; j++)
    {
        HALF_FLOAT* s = (HALF_FLOAT*)((char*)buffer + rowPitch * j);
        for(DWORD i = 0; i< width; i++)
        {
            d->R_s = s->R_s;
            d->R_e = s->R_e - 15 + 127;
            d->R_m = s->R_m << (23-10);
            d++;
            s++;
        }
    }
}

Обновление:

Демонтажные

; Listing generated by Microsoft (R) Optimizing Compiler Version 16.00.40219.01 

    TITLE   Utils.cpp
    .686P
    .XMM
    include listing.inc
    .model  flat

INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES

PUBLIC  ?ConvertImageFrom_R16_FLOAT_To_R32_FLOAT@@YAXPADPAXKKI@Z ; ConvertImageFrom_R16_FLOAT_To_R32_FLOAT
; Function compile flags: /Ogtp
;   COMDAT ?ConvertImageFrom_R16_FLOAT_To_R32_FLOAT@@YAXPADPAXKKI@Z
_TEXT   SEGMENT
_buffer$ = 8                        ; size = 4
tv83 = 12                       ; size = 4
_convertedData$ = 12                    ; size = 4
_width$ = 16                        ; size = 4
_height$ = 20                       ; size = 4
_rowPitch$ = 24                     ; size = 4
?ConvertImageFrom_R16_FLOAT_To_R32_FLOAT@@YAXPADPAXKKI@Z PROC ; ConvertImageFrom_R16_FLOAT_To_R32_FLOAT, COMDAT

; 323  : {

    push    ebp
    mov ebp, esp

; 343  :    for(DWORD j = 0; j< height; j++)

    mov eax, DWORD PTR _height$[ebp]
    push    esi
    mov esi, DWORD PTR _convertedData$[ebp]
    test    eax, eax
    je  SHORT $LN4@ConvertIma

; 324  :    union SINGLE_FLOAT {
; 325  :        struct {
; 326  :            unsigned __int32 R_m : 23;
; 327  :            unsigned __int32 R_e : 8;
; 328  :            unsigned __int32 R_s : 1;
; 329  :        };
; 330  :        struct {
; 331  :            float r;
; 332  :        };
; 333  :    };
; 334  :    C_ASSERT(sizeof(SINGLE_FLOAT) == 4);
; 335  :    struct HALF_FLOAT
; 336  :    {
; 337  :        unsigned __int16 R_m : 10;
; 338  :        unsigned __int16 R_e : 5;
; 339  :        unsigned __int16 R_s : 1;
; 340  :    };
; 341  :    C_ASSERT(sizeof(HALF_FLOAT) == 2);
; 342  :    SINGLE_FLOAT* d = (SINGLE_FLOAT*)convertedData;

    push    ebx
    mov ebx, DWORD PTR _buffer$[ebp]
    push    edi
    mov DWORD PTR tv83[ebp], eax
$LL13@ConvertIma:

; 344  :    {
; 345  :        HALF_FLOAT* s = (HALF_FLOAT*)((char*)buffer + rowPitch * j);
; 346  :        for(DWORD i = 0; i< width; i++)

    mov edi, DWORD PTR _width$[ebp]
    mov edx, ebx
    test    edi, edi
    je  SHORT $LN5@ConvertIma
    npad    1
$LL3@ConvertIma:

; 347  :        {
; 348  :            d->R_s = s->R_s;

    movzx   ecx, WORD PTR [edx]
    movzx   eax, WORD PTR [edx]
    shl ecx, 16                 ; 00000010H
    xor ecx, DWORD PTR [esi]
    shl eax, 16                 ; 00000010H
    and ecx, 2147483647             ; 7fffffffH
    xor ecx, eax
    mov DWORD PTR [esi], ecx

; 349  :            d->R_e = s->R_e - 15 + 127;

    movzx   eax, WORD PTR [edx]
    shr eax, 10                 ; 0000000aH
    and eax, 31                 ; 0000001fH
    add eax, 112                ; 00000070H
    shl eax, 23                 ; 00000017H
    xor eax, ecx
    and eax, 2139095040             ; 7f800000H
    xor eax, ecx
    mov DWORD PTR [esi], eax

; 350  :            d->R_m = s->R_m << (23-10);

    movzx   ecx, WORD PTR [edx]
    and ecx, 1023               ; 000003ffH
    shl ecx, 13                 ; 0000000dH
    and eax, -8388608               ; ff800000H
    or  ecx, eax
    mov DWORD PTR [esi], ecx

; 351  :            d++;

    add esi, 4

; 352  :            s++;

    add edx, 2
    dec edi
    jne SHORT $LL3@ConvertIma
$LN5@ConvertIma:

; 343  :    for(DWORD j = 0; j< height; j++)

    add ebx, DWORD PTR _rowPitch$[ebp]
    dec DWORD PTR tv83[ebp]
    jne SHORT $LL13@ConvertIma
    pop edi
    pop ebx
$LN4@ConvertIma:
    pop esi

; 353  :        }
; 354  :    }
; 355  : }

    pop ebp
    ret 0
?ConvertImageFrom_R16_FLOAT_To_R32_FLOAT@@YAXPADPAXKKI@Z ENDP ; ConvertImageFrom_R16_FLOAT_To_R32_FLOAT
_TEXT   ENDS

Ответы [ 10 ]

3 голосов
/ 23 октября 2016

Расширение набора команд x86 F16C добавляет аппаратную поддержку для преобразования векторов с плавающей запятой одинарной точности в / из векторов с плавающей запятой половинной точности.

Формат такой жеIEEE 754 двоичной точности 16, которую вы описываете .Я не проверял, что порядок байтов совпадает с вашей структурой, но это легко исправить при необходимости (с pshufb).

F16C поддерживается начиная с Intel IvyBridge и AMD Piledriver.(И имеет собственный бит функции CPUID, который должен проверять ваш код, иначе вернитесь к целочисленным сдвигам и тасованиям SIMD).

Встроенные функции для VCVTPS2PH :

__m128i _mm_cvtps_ph ( __m128 m1, const int imm);
__m128i _mm256_cvtps_ph(__m256 m1, const int imm);

Непосредственный байт является контролем округления.Компилятор может использовать его как преобразование и сохранение непосредственно в памяти (в отличие от большинства инструкций, которые могут опционально использовать операнд памяти, где операндом-источником может быть память вместо регистра).


VCVTPH2PS идет другим путем и аналогичен большинству других инструкций SSE (может использоваться между регистрами или в качестве нагрузки).

__m128 _mm_cvtph_ps ( __m128i m1);
__m256 _mm256_cvtph_ps ( __m128i m1)

F16C настолько эффективен, чтоВозможно, вы захотите оставить свое изображение в формате с половинной точностью и конвертировать на лету каждый раз, когда вам нужен вектор данных из него.Это отлично подходит для вашего кэша.

2 голосов
/ 01 апреля 2011

Вот несколько идей:

Поместите константы в const register переменные.

Некоторые процессоры не любят извлекать константы из памяти;это неудобно и может занять много циклов обучения.

Развертывание цикла

Повторите операторы в цикле и увеличьте приращение.
Процессоры предпочитают непрерывные инструкции;скачки и ветки злят их.

Предварительная выборка данных (или загрузка кэша)

Используйте больше переменных в цикле и объявите их как volatile, чтобы компилятор не оптимизировал их:

SINGLE_FLOAT* d = (SINGLE_FLOAT*)convertedData;
SINGLE_FLOAT* d1 = d + 1;
SINGLE_FLOAT* d2 = d + 2;
SINGLE_FLOAT* d3 = d + 3;
for(DWORD j = 0; j< height; j++)
{
    HALF_FLOAT* s = (HALF_FLOAT*)((char*)buffer + rowPitch * j);
    HALF_FLOAT* s1 = (HALF_FLOAT*)((char*)buffer + rowPitch * (j + 1));
    HALF_FLOAT* s2 = (HALF_FLOAT*)((char*)buffer + rowPitch * (j + 2));
    HALF_FLOAT* s3 = (HALF_FLOAT*)((char*)buffer + rowPitch * (j + 3));
    for(DWORD i = 0; i< width; i += 4)
    {
        d->R_s = s->R_s;
        d->R_e = s->R_e - 15 + 127;
        d->R_m = s->R_m << (23-10);
        d1->R_s = s1->R_s;
        d1->R_e = s1->R_e - 15 + 127;
        d1->R_m = s1->R_m << (23-10);
        d2->R_s = s2->R_s;
        d2->R_e = s2->R_e - 15 + 127;
        d2->R_m = s2->R_m << (23-10);
        d3->R_s = s3->R_s;
        d3->R_e = s3->R_e - 15 + 127;
        d3->R_m = s3->R_m << (23-10);
        d += 4;
        d1 += 4;
        d2 += 4;
        d3 += 4;
        s += 4;
        s1 += 4;
        s2 += 4;
        s3 += 4;
    }
}
2 голосов
/ 01 апреля 2011

Доступ к битовым полям в памяти может быть очень сложным, в зависимости от архитектуры, конечно.

Вы могли бы добиться лучшей производительности, если бы вы создали объединение с плавающей точкой и 32-битным целым числом и просто выполнидекомпозиция и композиция с использованием локальных переменных.Таким образом, сгенерированный код может выполнить всю операцию, используя только регистры процессора.

1 голос
/ 01 апреля 2011

Вы должны быть в состоянии свести это к одной инструкции для микросхем, которые используют предстоящий набор CVT16 . Согласно той статье в Википедии:

The CVT16 instructions allow conversion of floating point vectors between single precision and half precision.

1 голос
/ 01 апреля 2011

Вы обрабатываете данные как двумерный массив.Если вы подумаете о том, как оно расположено в памяти, вы сможете обработать его как одномерный массив и сэкономить немного ресурсов, используя один цикл вместо вложенных циклов.код и убедитесь, что оптимизация компилятора сработала и не пересчитывается (15 + 127) сотни раз.

1 голос
/ 01 апреля 2011

SSE Intrinsics - отличная идея.Прежде чем идти по этому пути, вы должны

  • посмотреть код сборки, сгенерированный компилятором, (есть ли потенциал для оптимизации?)

  • найдите в документации по компилятору, как автоматически генерировать код SSE,

  • найдите в документации своей библиотеки программного обеспечения (или там, где появился 16-битный тип с плавающей запятой) функцию для массового преобразования этого типа.(преобразование в 64-битное число с плавающей запятой также может быть полезным.) Вы, скорее всего, не первый человек, столкнувшийся с этой проблемой!

Если все это не помогло, отправляйтесь и попытайте удачу с некоторымиSSE присущи.Чтобы понять, здесь - это некоторый SSE-код для преобразования из 32-битной с плавающей запятой в 16-битную.(вы хотите обратное)

Помимо SSE вы должны также рассмотреть возможность многопоточности и выгрузки задачи в графический процессор.

1 голос
/ 01 апреля 2011

циклы не зависят друг от друга, поэтому вы можете легко распараллелить этот код, используя SIMD или OpenMP, простая версия будет разделять верхнюю половину и нижнюю половину изображения на два потока, работая одновременно.

0 голосов
/ 01 апреля 2011

Я подозреваю, что эта операция будет уже узким местом при доступе к памяти, и повышение ее эффективности (например, с помощью SSE) не заставит ее выполняться быстрее.Однако это только подозрение.

Другие вещи, которые можно попробовать, предполагая x86 / x64, могут быть:

  • Не d++ и s++, но используйте d[i] и s[i] на каждой итерации.(Затем, конечно, поднять d после каждой строки развертки.) Поскольку элементы d составляют 4 байта, а элементы s 2, эту операцию можно сложить в вычисление адреса.(К сожалению, я не могу гарантировать, что это обязательно сделает выполнение более эффективным.)
  • Удалите операции битового поля и выполните операции вручную.(При извлечении сдвигайте сначала и маскируйте вторую, чтобы максимизировать вероятность того, что маска может вписаться в небольшое непосредственное значение.)
  • Развернуть цикл, хотя с циклом, столь же легко предсказуемым, как этот, он может неимеют большое значение.
  • Считайте по каждой строке от width до нуля.Это мешает компилятору извлекать width каждый раз.Вероятно, более важно для x86, потому что в нем так мало регистров.(Если процессору нравится мое предложение "d[i] и s[i]", вы можете сделать ширину со знаком, считать вместо width-1 и идти назад.)

Все это будет быстреепопробуйте выполнить преобразование в SSE и, надеюсь, сделаете его связанным с памятью, если это еще не сделано, в этот момент вы можете отказаться.

Наконец, если вывод находится в комбинированной памяти записи (например, этобуфер текстур или вершин или что-то доступное через AGP, PCI Express или что-то еще, что есть у ПК в наши дни), это может привести к снижению производительности, в зависимости от того, какой код скомпилирован компилятором для внутреннего цикла.Таким образом, в этом случае вы можете получить лучшие результаты, конвертируя каждую строку развертки в локальный буфер, а затем используя memcpy, чтобы скопировать ее в конечный пункт назначения.

0 голосов
/ 01 апреля 2011

Функция делает только несколько небольших вещей.Оптимизация будет очень трудно сократить время, но, как кто-то уже сказал, распараллеливание имеет многообещающие перспективы.

Проверьте, сколько промахов кеша вы получаете.Если данные поступают и выгружаются, возможно, вы сможете ускорить их, применив больше интеллектуального подхода к упорядочению, чтобы минимизировать перестановки кэша.

Также рассмотрим макрооптимизацию.Существуют ли какие-либо избыточности в вычислениях данных, которых можно было бы избежать (например, кэширование старых результатов вместо их пересчета при необходимости)?Вам действительно нужно преобразовать весь набор данных или вы можете просто преобразовать нужные биты?Я не знаю ваше приложение, поэтому я просто дико догадываюсь здесь, но могут быть возможности для такого рода оптимизации.

0 голосов
/ 01 апреля 2011

Я не знаю, как работает SSE, но было бы интересно увидеть разборку вашего внутреннего цикла. Старый способ (который может не сильно помочь, но его было бы легко опробовать) состоял бы в том, чтобы уменьшить количество итераций, выполнив два внутренних цикла: один, который выполняет N (скажем, 32) повторов обработки (счетчик циклов width / N), а затем один, чтобы завершить остаток (число циклов ширины% N) ... с этими div-значениями и модулями, вычисленными вне первого цикла, чтобы избежать их пересчета. Извиняюсь, если это звучит очевидно!

...