==
тесты для ссылочного равенства (независимо от того, являются ли они одним и тем же объектом).
.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 .
Действительно, комментарии верны: компилятор может сделать для вас векторизацию. Я немного изменил ваш код для улучшения авто-векторизации. В 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);
}
}
Это приводит к аналогичным результатам , что и векторизованный код.
Добавление к ответу @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. Так что если вы хотите получить абсолютную последнюю каплю перфекта (и иметь фиксированную архитектуру), то возможно что-то подобное вышеописанному. Однако это действительно небольшое улучшение, так как компиляторы уже справляются с этим почти идеально , и поэтому я бы порекомендовал не делать этого и просто доверять компилятору.
Если вы думаете, что можете улучшить компилятор, не забывайте всегда проверять и профилировать, чтобы убедиться, что вы на самом деле.
В отличие от оптимизированных вручную подходов, представленных в замечательных ответах Вима и Майка, давайте также кратко рассмотрим, что даст нам полностью ванильная реализация 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
, что действительно неплохо…