Какой самый быстрый способ преобразовать большой массив массива char8 в short16?

== тесты для ссылочного равенства (независимо от того, являются ли они одним и тем же объектом).

.equals() тесты для равенства значений (независимо от того, являются ли они логически «равными»).

Objects.equals () проверяет наличие null перед вызовом .equals(), поэтому вам не нужно (доступно с JDK7, также доступным в Guava ).

String.contentEquals () сравнивает содержимое String с содержимым любого CharSequence (доступно с Java 1.5).

Следовательно, если вы хотите проверить, имеет ли две строки одно и то же значение, вы, вероятно, захотите использовать Objects.equals().

// These two have the same value
new String("test").equals("test") // --> true 

// ... but they are not the same object
new String("test") == "test" // --> false 

// ... neither are these
new String("test") == new String("test") // --> false 

// ... but these are because literals are interned by 
// the compiler and thus refer to the same object
"test" == "test" // --> true 

// ... string literals are concatenated by the compiler
// and the results are interned.
"test" == "te" + "st" // --> true

// ... but you should really just call Objects.equals()
Objects.equals("test", new String("test")) // --> true
Objects.equals(null, "test") // --> false
Objects.equals(null, null) // --> true

Вы почти всегда хотите использовать Objects.equals(). В редкой ситуации, когда вы знаете, что имеете дело с интернированными строками, вы можете использовать ==.

Из JLS 3.10. 5. Строковые литералы :

Кроме того, строковый литерал всегда ссылается на тот же экземпляр класса String. Это связано с тем, что строковые литералы, или, в более общем смысле, строки, которые являются значениями константных выражений ( §15.28 ), «интернированы», чтобы обмениваться уникальными экземплярами, используя метод String.intern.

. Подобные примеры также можно найти в JLS 3.10.5-1 .

2
задан Ngọc Khánh Nguyễn 17 January 2019 в 10:30
поделиться

3 ответа

Действительно, комментарии верны: компилятор может сделать для вас векторизацию. Я немного изменил ваш код для улучшения авто-векторизации. В gcc -O3 -march=haswell -std=c++14 (gcc версии 8.2) следующий код:

#include <cstdint>
#include <immintrin.h>

void cvt_uint8_int16(uint16_t * __restrict__ canvas, uint8_t * __restrict__ addon, int64_t count) {
    int64_t i;
    /* If you know that n is always a multiple of 32 then insert       */
    /* n = n & 0xFFFFFFFFFFFFFFE0u;                                    */
    /* This leads to cleaner code. Now assume n is a multiple of 32:   */
    count = count & 0xFFFFFFFFFFFFFFE0u;                               
    for (i = 0; i < count; i++){
        canvas[i] += static_cast<uint16_t>(addon[i]);
    }
}

компилируется в:

cvt_uint8_int16(unsigned short*, unsigned char*, long):
        and     rdx, -32
        jle     .L5
        add     rdx, rsi
.L3:
        vmovdqu ymm2, YMMWORD PTR [rsi]
        add     rsi, 32
        add     rdi, 64
        vextracti128    xmm1, ymm2, 0x1
        vpmovzxbw       ymm0, xmm2
        vpaddw  ymm0, ymm0, YMMWORD PTR [rdi-64]
        vpmovzxbw       ymm1, xmm1
        vpaddw  ymm1, ymm1, YMMWORD PTR [rdi-32]
        vmovdqu YMMWORD PTR [rdi-64], ymm0
        vmovdqu YMMWORD PTR [rdi-32], ymm1
        cmp     rdx, rsi
        jne     .L3
        vzeroupper
.L5:

Компилятор Clang создает код , который немного отличается : Он загружает 128-битные (char) векторы и конвертирует их с помощью vpmovzxbw. Компилятор gcc загружает 256-битные (char) векторы и преобразует верхний и нижний 128-битные по отдельности, что, вероятно, немного менее эффективно. Тем не менее, ваша проблема, скорее всего, ограничена пропускной способностью (так как длина> 1000000).

Вы также можете векторизовать код с помощью встроенных функций (не проверено):

void cvt_uint8_int16_with_intrinsics(uint16_t * __restrict__ canvas, uint8_t * __restrict__ addon, int64_t count) {
    int64_t i;
    /* Assume n is a multiple of 16  */
    for (i = 0; i < count; i=i+16){
        __m128i x     = _mm_loadu_si128((__m128i*)&addon[i]);
        __m256i y     = _mm256_loadu_si256((__m256i*)&canvas[i]);
        __m256i x_u16 = _mm256_cvtepu8_epi16(x);
        __m256i sum   = _mm256_add_epi16(y, x_u16);
                _mm256_storeu_si256((__m256i*)&canvas[i], sum);
    }
}

Это приводит к аналогичным результатам , что и векторизованный код.

0
ответ дан wim 17 January 2019 в 10:30
поделиться

Добавление к ответу @wim (который является хорошим ответом ) и принятие во внимание комментария @Bathsheba, стоит того, чтобы доверять компилятору , но также проверять, на что выводит ваш компилятор оба учатся тому, как это делать, а также проверяют, что это делает то, что вы хотите. Выполнение слегка измененной версии вашего кода через godbolt (для msvc, gcc и clang) дает некоторые не идеальные ответы.

Это особенно верно, если вы ограничиваете себя SSE2 и ниже, что предполагает этот ответ (и то, что я проверял)

Все компиляторы как векторизируют, так и разворачивают код и используют punpcklbw «распаковать» uint8_t в uint16_t, а затем запустить добавление и сохранение SIMD. Это хорошо. Однако MSVC имеет тенденцию излишне разливаться во внутреннем цикле, и clang использует только punpcklbw, а не punpckhbw, что означает, что он загружает исходные данные дважды. GCC получает правильную часть SIMD, но имеет более высокие издержки для ограничений цикла.

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

static inline void adder2(uint16_t *canvas, uint8_t *addon, uint64_t count)
{
    uint64_t count32 = (count / 32) * 32;
    __m128i zero = _mm_set_epi32(0, 0, 0, 0);
    uint64_t i = 0;
    for (; i < count32; i+= 32)
    {
        uint8_t* addonAddress = (addon + i);

        // Load data 32 bytes at a time and widen the input
        // to `uint16_t`'sinto 4 temp xmm reigsters.
        __m128i input = _mm_loadu_si128((__m128i*)(addonAddress + 0));
        __m128i temp1 = _mm_unpacklo_epi8(input, zero);
        __m128i temp2 = _mm_unpackhi_epi8(input, zero);
        __m128i input2 = _mm_loadu_si128((__m128i*)(addonAddress + 16));
        __m128i temp3 = _mm_unpacklo_epi8(input2, zero);
        __m128i temp4 = _mm_unpackhi_epi8(input2, zero);

        // Load data we need to update
        uint16_t* canvasAddress = (canvas + i);
        __m128i canvas1 = _mm_loadu_si128((__m128i*)(canvasAddress + 0));
        __m128i canvas2 = _mm_loadu_si128((__m128i*)(canvasAddress + 8));
        __m128i canvas3 = _mm_loadu_si128((__m128i*)(canvasAddress + 16));
        __m128i canvas4 = _mm_loadu_si128((__m128i*)(canvasAddress + 24));

        // Update the values
        __m128i output1 = _mm_add_epi16(canvas1, temp1);
        __m128i output2 = _mm_add_epi16(canvas2, temp2);
        __m128i output3 = _mm_add_epi16(canvas3, temp3);
        __m128i output4 = _mm_add_epi16(canvas4, temp4);

        // Store the values
        _mm_storeu_si128((__m128i*)(canvasAddress + 0), output1);
        _mm_storeu_si128((__m128i*)(canvasAddress + 8), output2);
        _mm_storeu_si128((__m128i*)(canvasAddress + 16), output3);
        _mm_storeu_si128((__m128i*)(canvasAddress + 24), output4);
    }

    // Mop up
    for (; i<count; i++)
        canvas[i] += static_cast<uint16_t>(addon[i]);
}

Изучение выходных данных для этого строго лучше, чем у любого из gcc / clang / MSVC. Так что если вы хотите получить абсолютную последнюю каплю перфекта (и иметь фиксированную архитектуру), то возможно что-то подобное вышеописанному. Однако это действительно небольшое улучшение, так как компиляторы уже справляются с этим почти идеально , и поэтому я бы порекомендовал не делать этого и просто доверять компилятору.

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

0
ответ дан Mike Vine 17 January 2019 в 10:30
поделиться

В отличие от оптимизированных вручную подходов, представленных в замечательных ответах Вима и Майка, давайте также кратко рассмотрим, что даст нам полностью ванильная реализация C ++:

std::transform(addon, addon + count, canvas, canvas, std::plus<void>());

Попробуйте здесь . Вы увидите, что даже без каких-либо реальных усилий с вашей стороны, компилятор уже может создавать векторизованный код, что весьма неплохо, учитывая, что он не может делать какие-либо предположения относительно выравнивания и размера ваших буферов, а также есть некоторые потенциальные проблемы с наложением ( из-за использования uint8_t, который, к сожалению, заставляет компилятор предполагать, что указатель может иметь псевдоним для любого другого объекта). Также обратите внимание, что код в основном идентичен тому, что вы получите от реализации в стиле C (в зависимости от компилятора, версия C ++ содержит несколько инструкций больше или меньше инструкций)

void f(uint16_t* canvas, const uint8_t* addon, size_t count)
{
    for (size_t i = 0; i < count; ++i)
        canvas[i] += addon[i];
}

Однако универсальное решение C ++ работает с любой комбинацией различных типов контейнеров и типов элементов, если только можно добавить типы элементов. Таким образом, как также указывалось в других ответах, хотя, безусловно, можно получить чуть более эффективную реализацию от ручной оптимизации, можно проделать долгий путь, просто написав простой код C ++ (если все сделано правильно). Прежде чем прибегать к ручному написанию встроенных функций SSE, подумайте, что универсальное решение C ++ является более гибким, более простым в обслуживании и, особенно, более переносимым. Простым переключением переключателя целевой архитектуры вы можете заставить его генерировать код аналогичного качества не только для SSE, но и для AVX или даже для ARM с NEON и любыми другими наборами команд, которые могут вам понадобиться. Если вам нужно, чтобы ваш код был идеальным вплоть до последней инструкции для одного конкретного варианта использования на одном конкретном процессоре, тогда да, возможно, стоит использовать встроенную или даже встроенную сборку. Но в целом, я бы также предложил вместо этого сосредоточиться на написании своего кода на C ++ таким образом, чтобы компилятор мог и направлял сборку, которую вы хотите, а не генерировал сборку самостоятельно. Например, используя (нестандартный, но общедоступный) ограничивающий квалификатор и заимствуя хитрость, сообщая компилятору, что ваш count всегда кратен 32

void f(std::uint16_t* __restrict__ canvas, const std::uint8_t* __restrict__ addon, std::size_t count)
{
    assert(count % 32 == 0);
    count = count & -32;
    std::transform(addon, addon + count, canvas, canvas, std::plus<void>());
}

, вы получите (-std=c++17 -DNDEBUG -O3 -mavx)

f(unsigned short*, unsigned char const*, unsigned long):    
        and     rdx, -32
        je      .LBB0_3
        xor     eax, eax
.LBB0_2:                                # =>This Inner Loop Header: Depth=1
        vpmovzxbw       xmm0, qword ptr [rsi + rax] # xmm0 = mem[0],zero,mem[1],zero,mem[2],zero,mem[3],zero,mem[4],zero,mem[5],zero,mem[6],zero,mem[7],zero
        vpmovzxbw       xmm1, qword ptr [rsi + rax + 8] # xmm1 = mem[0],zero,mem[1],zero,mem[2],zero,mem[3],zero,mem[4],zero,mem[5],zero,mem[6],zero,mem[7],zero
        vpmovzxbw       xmm2, qword ptr [rsi + rax + 16] # xmm2 = mem[0],zero,mem[1],zero,mem[2],zero,mem[3],zero,mem[4],zero,mem[5],zero,mem[6],zero,mem[7],zero
        vpmovzxbw       xmm3, qword ptr [rsi + rax + 24] # xmm3 = mem[0],zero,mem[1],zero,mem[2],zero,mem[3],zero,mem[4],zero,mem[5],zero,mem[6],zero,mem[7],zero
        vpaddw  xmm0, xmm0, xmmword ptr [rdi + 2*rax]
        vpaddw  xmm1, xmm1, xmmword ptr [rdi + 2*rax + 16]
        vpaddw  xmm2, xmm2, xmmword ptr [rdi + 2*rax + 32]
        vpaddw  xmm3, xmm3, xmmword ptr [rdi + 2*rax + 48]
        vmovdqu xmmword ptr [rdi + 2*rax], xmm0
        vmovdqu xmmword ptr [rdi + 2*rax + 16], xmm1
        vmovdqu xmmword ptr [rdi + 2*rax + 32], xmm2
        vmovdqu xmmword ptr [rdi + 2*rax + 48], xmm3
        add     rax, 32
        cmp     rdx, rax
        jne     .LBB0_2
.LBB0_3:
        ret

, что действительно неплохо…

0
ответ дан Michael Kenzel 17 January 2019 в 10:30
поделиться