打印一个__m128i变量

31

我正在尝试使用指令集编写代码,以下是一个执行加法的示例代码

使用的编译器: icc

#include<stdio.h>
#include<emmintrin.h>
int main()
{
        __m128i a = _mm_set_epi32(1,2,3,4);
        __m128i b = _mm_set_epi32(1,2,3,4);
        __m128i c;
        c = _mm_add_epi32(a,b);
        printf("%d\n",c[2]);
        return 0;
}

我遇到以下错误:

test.c(9): error: expression must have pointer-to-object type
        printf("%d\n",c[2]);

如何打印类型为 __m128i 的变量 c 中的值?


2
还要注意,__m128i 没有任何关于存储的类型信息。它可以是8位整数、16位整数、32位等等... 一些编译器支持.m128i_i32字段扩展。但这绝对不是标准的,也不在GCC中。 - Mysticial
1
如何使用gcc打印__uint128_t数字? - jfs
1
请注意,一些编译器已经内置了对SIMD类型的printf支持,例如苹果版本的gcc、clang等都支持使用%vld打印一个__m128i作为4个32位整数。 - Paul R
我正在使用英特尔编译器。 - arunmoezhi
有没有一种方法可以进行掩码加法。比如说,我只想存储交替元素(c[0],c[2])? - arunmoezhi
0жҳҜеҠ жі•зҡ„еҚ•дҪҚе…ғзҙ гҖӮеӣ жӯӨпјҢеұҸи”ҪжҺүе…¶дёӯдёҖдёӘиҫ“е…Ҙж“ҚдҪңж•°д»ҘеҸҠзӣёеә”зҡ„е…ғзҙ пјҢйӮЈд№Ҳc = a + (b & mask)дёӯзҡ„з»“жһңе°Ҷдёәc = a + 0 = aгҖӮ - Peter Cordes
4个回答

29

使用此函数来打印它们:

#include <stdint.h>
#include <string.h>

void print128_num(__m128i var)
{
    uint16_t val[8];
    memcpy(val, &var, sizeof(val));
    printf("Numerical: %i %i %i %i %i %i %i %i \n", 
           val[0], val[1], val[2], val[3], val[4], val[5], 
           val[6], val[7]);
}

在打印之前,您需要将128位拆分为16位(或32位)。

如果您有64位支持,则这是一种64位拆分和打印的方法:

#include <inttypes.h>

void print128_num(__m128i var) 
{
    int64_t v64val[2];
    memcpy(v64val, &var, sizeof(v64val));
    printf("%.16llx %.16llx\n", v64val[1], v64val[0]);
}

注意:在MSVC中直接将&var强制转换为int*uint16_t*也可以工作,但这违反了严格别名规则,属于未定义行为。使用memcpy是符合标准的方法,且在最小化优化情况下,编译器将生成完全相同的二进制代码。


1
如果您想要整数,请将 llx 替换为 lld - askmish
它可以工作。我使用uint32_t来打印32位整数。但输出是反向的。我得到的是8,6,4,2而不是2,4,6,8。_mm_add_epi32是否以相反的顺序存储值? - arunmoezhi
3
可能不太常见。更常见的方式是使用 _mm_extract_epi32 或将其存储到本地数组中。您还可以将其分配给一个 __m128i 和一个数组的 union。如果测试/调试输出时它恰好起作用,那么这样做也是可以的。然而,调试器比调试输出更容易显示矢量内容。 - Peter Cordes
1
另外:__m128i bp = _mm_set_epi32(0xFF, 0xfe,0xfa,0xfb); std::cout << std::setfill('0') << std::hex<<std::setw(16)<< bp.m128i_i64[1]<<std::setw(16)<< bp.m128i_i64[0]; - Алексей Неудачин
int *val = (int*)&var 怎么样?这样你就不需要使用 memcpy 了。 - Nanashi No Gombe
@NanashiNoGombe:这是一个严格别名违规,指向一个不同类型 (__m128i) 对象的 int*。请参见编辑历史记录和我的答案。使用 memcpy 而不是 _mm_storeu_si128 很奇怪,但无论哪种方式编译器都希望能够优化成相同的汇编代码,并且不会实际调用 memcpy 库函数(除了在调试构建中)。 - Peter Cordes

20
  • 可移植于gcc/clang/ICC/MSVC,C和C++。
  • 在所有优化级别下完全安全:没有严格别名违规UB
  • 以u8、u16、u32或u64元素的十六进制形式打印(基于@AG1的答案
  • 按内存顺序打印(最低有效位元素先,例如_mm_setr_epiX)。如果您喜欢按照英特尔手册使用的相同顺序打印,则反转数组索引,其中最高有效元素在左侧(例如_mm_set_epiX)。相关:显示向量寄存器的约定

使用__m128i*int数组中加载是安全的,因为__m128类型的定义允许别名,就像ISO C的unsigned char*一样。 (例如,在gcc的头文件中,定义包括__attribute__((may_alias))。)

反过来不安全(将指向__m128i对象的一部分的int*)。 MSVC保证是安全的,但GCC / clang不保证。(-fstrict-aliasing默认开启)。 它有时在GCC / clang中起作用,但为什么要冒险呢? 它有时甚至会干扰优化;请参见this Q&A。 另请参见Is `reinterpret_cast`ing between hardware SIMD vector pointer and the corresponding type an undefined behavior?

请参见GCC AVX _m256i cast to int array leads to wrong values,其中提供了GCC破坏指向__m256iint*代码的真实示例。


(uint32_t*) &my_vector 违反了 C 和 C++ 的别名规则,不能保证其按照预期工作。将其存储到本地数组中然后进行访问是安全的。大多数编译器甚至会进行优化,因此您可以直接从 xmm 寄存器移动 movq / pextrq 到整数寄存器,而不是进行实际的存储/重载。

在 Godbolt 编译器探索器上的源代码和汇编输出:证明它可以使用 MSVC 编译,等等。

#include <immintrin.h>
#include <stdint.h>
#include <stdio.h>

#ifndef __cplusplus
#include <stdalign.h>   // C11 defines _Alignas().  This header defines alignas()
#endif

void p128_hex_u8(__m128i in) {
    alignas(16) uint8_t v[16];
    _mm_store_si128((__m128i*)v, in);
    printf("v16_u8: %x %x %x %x | %x %x %x %x | %x %x %x %x | %x %x %x %x\n",
           v[0], v[1],  v[2],  v[3],  v[4],  v[5],  v[6],  v[7],
           v[8], v[9], v[10], v[11], v[12], v[13], v[14], v[15]);
}

void p128_hex_u16(__m128i in) {
    alignas(16) uint16_t v[8];
    _mm_store_si128((__m128i*)v, in);
    printf("v8_u16: %x %x %x %x,  %x %x %x %x\n", v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7]);
}

void p128_hex_u32(__m128i in) {
    alignas(16) uint32_t v[4];
    _mm_store_si128((__m128i*)v, in);
    printf("v4_u32: %x %x %x %x\n", v[0], v[1], v[2], v[3]);
}

void p128_hex_u64(__m128i in) {
    alignas(16) unsigned long long v[2];  // uint64_t might give format-string warnings with %llx; it's just long in some ABIs
    _mm_store_si128((__m128i*)v, in);
    printf("v2_u64: %llx %llx\n", v[0], v[1]);
}

如果需要在C99或C++03或更早的版本中具有可移植性(即没有C11 / C++11),则删除alignas()并使用storeu代替store。 或者使用__attribute__((aligned(16)))__declspec( align(16) )

(如果您正在编写具有内部函数的代码,则应使用最新的编译器版本。新的编译器通常比旧的编译器生成更好的汇编代码,包括SSE / AVX内部函数。但是,也许你想使用gcc-6.3和-std=gnu++03 C++03模式,用于尚未准备好使用C++11或其他内容的代码库。)


调用所有 4 个函数后的示例输出

// source used:
__m128i vec = _mm_setr_epi8(1, 2, 3, 4, 5, 6, 7,
                            8, 9, 10, 11, 12, 13, 14, 15, 16);

// output:

v2_u64: 0x807060504030201 0x100f0e0d0c0b0a09
v4_u32: 0x4030201 0x8070605 0xc0b0a09 0x100f0e0d
v8_u16: 0x201 0x403 0x605 0x807  | 0xa09 0xc0b 0xe0d 0x100f
v16_u8: 0x1 0x2 0x3 0x4 | 0x5 0x6 0x7 0x8 | 0x9 0xa 0xb 0xc | 0xd 0xe 0xf 0x10

如果您想要在输出宽度一致时填充前导零,请调整格式字符串。请参阅printf(3)


5

我知道这个问题标记为C语言,但是当寻找C++解决方案时,这也是最好的搜索结果。

因此,以下是可能的C++实现:

#include <string>
#include <cstring>
#include <sstream>

#if defined(__SSE2__)
template <typename T>
std::string __m128i_toString(const __m128i var) {
    std::stringstream sstr;
    T values[16/sizeof(T)];
    std::memcpy(values,&var,sizeof(values)); //See discussion below
    if (sizeof(T) == 1) {
        for (unsigned int i = 0; i < sizeof(__m128i); i++) { //C++11: Range for also possible
            sstr << (int) values[i] << " ";
        }
    } else {
        for (unsigned int i = 0; i < sizeof(__m128i) / sizeof(T); i++) { //C++11: Range for also possible
            sstr << values[i] << " ";
        }
    }
    return sstr.str();
}
#endif

使用方法:

#include <iostream>
[..]
__m128i x
[..]
std::cout << __m128i_toString<uint8_t>(x) << std::endl;
std::cout << __m128i_toString<uint16_t>(x) << std::endl;
std::cout << __m128i_toString<uint32_t>(x) << std::endl;
std::cout << __m128i_toString<uint64_t>(x) << std::endl;

结果:

141 114 0 0 0 0 0 0 151 104 0 0 0 0 0 0
29325 0 0 0 26775 0 0 0
29325 0 26775 0
29325 26775

注意:有一种简单的方法可以避免if (size(T)==1),请参见https://dev59.com/wmUq5IYBdhLWcg3wQ-Tk#28414758

@PeterCordes 但是,memcpy 不也是一种有效的替代方法吗? - Antonio
是的,这样也可以。但我认为使用_mm_storeu_si128编译器通常会做得更好。对于memcpy,如果至少有一个编译器将__m128溢出到堆栈,然后从堆栈复制到数组,我不会感到惊讶。(虽然可能不会,在线复制微不足道的内容是他们通常擅长的事情)。在我看来,_mm_storeu绝对是更符合惯用法的方法。实际上,它与大多数编译器完全优化为movq/pextrq或其他指令(即使存储/重新加载更好:许多微小的元素)。 - Peter Cordes
@PeterCordes 感谢您的见解!一个要点是,在像这样的流函数中,完美的优化并不是真正重要的。我将调整我的答案使用memcpy,这更符合我的编码风格,并且我相信更容易理解。 - Antonio
我认为storeu更容易理解,因为使用内部函数的代码肯定会使用它。但是好吧,如果你喜欢用memcmp进行类型转换,那就继续吧。 - Peter Cordes
1
是的,只有在使用非2次幂类而不是uint*_t时才会出现性能问题。 为了可读性,保持原样是有意义的。(特别是因为使用std::string和字符串流来打印向量并没有什么高性能之处。)如果您将其放入库供人们使用而不查看它,而不是SO答案,则会做出不同的选择。 - Peter Cordes
显示剩余11条评论

2
#include<stdio.h>
#include<emmintrin.h>
int main()
{
    __m128i a = _mm_set_epi32(1,2,3,4);
    __m128i b = _mm_set_epi32(1,2,3,4);
    __m128i c;

    const int32_t* q; 
    //add a pointer 
    c = _mm_add_epi32(a,b);

    q = (const int32_t*) &c;
    printf("%d\n",q[2]);
    //printf("%d\n",c[2]);
    return 0;
}

尝试使用这段代码。


@NateEldredge:我确定这不是严格合法的(除非你使用-fno-strict-aliasing或类似选项)。我发布了一个安全的答案。 - Peter Cordes
@PeterCordes,关于你的评论“这不是严格合法的”,有没有什么方法可以得到编译器警告?我尝试使用-Wstrict-aliasing,但没有收到任何警告。我还尝试了-fsanitize=undefined来检查运行时警告或错误,但都没有收到。 - dannyadam
1
@dannyadam:有趣,但似乎这些检查无法捕获明显的严格别名违规行为:https://godbolt.org/z/qo4vre,例如对于一个`unsigned long long arr[10];数组,return *(5 + (int*)arr);`。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接