各位同仁、技术爱好者们:
欢迎来到今天的技术讲座。今天,我们将深入探讨一个在高性能计算领域至关重要的话题:如何利用 SIMD 内部函数(Intrinsics)实现高性能图像处理算子。在当今这个视觉信息爆炸的时代,从智能手机的实时美颜到自动驾驶的视觉感知,从医疗影像分析到卫星图像处理,图像处理无处不在。而其核心挑战之一,就是如何在保证处理质量的同时,大幅提升处理速度。
在图像处理的诸多场景中,我们常常面临对大量像素数据进行重复、独立的计算。传统的串行处理方式在面对海量数据时显得力不从心。这时,并行计算就成为了突破性能瓶颈的关键。今天,我们将聚焦于 CPU 层面的一种强大并行技术——SIMD (Single Instruction, Multiple Data),特别是通过直接使用编译器提供的 SIMD 内部函数,来榨取硬件的极致性能。这并非简单的编译器自动优化,而是需要我们深入理解硬件架构,精细化地控制数据流和指令执行,以达到“深度优化”的目标。
本次讲座旨在为您提供一套系统性的知识体系和实用的编程技巧,让您能够亲自动手,将那些耗时的图像处理核心算子,从“蜗牛”变为“猎豹”。
一、 SIMD 的核心概念:单指令、多数据
在深入探讨 SIMD Intrinsics 之前,我们首先需要理解 SIMD 的基本原理。
1.1 什么是 SIMD?
SIMD,全称 Single Instruction, Multiple Data,即“单指令、多数据”。它是一种并行计算模型,与传统的 SISD (Single Instruction, Single Data) 模型相对。在 SISD 中,CPU 的一个指令只能处理一个数据元素。而在 SIMD 架构下,一个指令可以同时对多个数据元素执行相同的操作。
想象一下,你有一队工人需要将一批货物从 A 点搬到 B 点。
- SISD:只有一个工人,他一次只能搬运一件货物,然后重复这个动作。
- SIMD:你有一排工人,他们同时伸出手,每人搬运一件货物,然后同时移动。
在 CPU 层面,这意味着处理器内部的寄存器不再是只能存储一个整数或一个浮点数,而是可以存储一个“向量”,这个向量包含多个相同类型的小数据。例如,一个 128 位的 SIMD 寄存器可以同时存储 4 个 32 位浮点数,或 8 个 16 位整数,或 16 个 8 位整数。当执行一个 SIMD 指令时,比如加法指令,它会同时对这整个向量中的所有对应元素执行加法操作。
1.2 为什么 SIMD 对图像处理如此重要?
图像本质上是由大量像素组成的二维网格。每个像素通常包含亮度、颜色(R, G, B, A)等信息。图像处理操作,如亮度调整、对比度增强、模糊、锐化、颜色转换等,往往具有以下特点:
- 数据并行性高:对一个像素的计算通常不依赖于其他像素的计算结果(或只依赖于其局部邻域),这意味着大量像素可以并行处理。
- 数据密集型:一张高清图像可能包含数百万甚至数千万像素,每个像素又包含多个字节的数据。
- 重复性操作:相同的算术或逻辑操作会被反复应用于每个像素或像素块。
这些特点与 SIMD 的工作原理完美契合。SIMD 能够一次性处理多个像素或像素通道的数据,极大地减少了指令的数量和内存访问的延迟,从而显著提升图像处理的吞吐量。
1.3 SIMD 指令集的发展历程
SIMD 技术并非一蹴而就,而是随着处理器技术的发展逐步演进的。以下是主流 x86 架构下的主要 SIMD 指令集:
| 指令集 | 首次引入 | 向量寄存器宽度 | 主要功能 |
|---|---|---|---|
| MMX | 1997 | 64 位 | 整数运算,浮点数共用寄存器 |
| SSE | 1999 | 128 位 | 浮点数运算(单精度),独立浮点寄存器 |
| SSE2 | 2001 | 128 位 | 整数运算,双精度浮点运算 |
| SSE3 | 2004 | 128 位 | 复杂算术指令,如水平加法 |
| SSSE3 | 2006 | 128 位 | 字节混洗、绝对值等 |
| SSE4.1 | 2007 | 128 位 | 点积、插入/提取、最大/最小值等 |
| SSE4.2 | 2008 | 128 位 | 字符串/文本处理 |
| AVX | 2011 | 256 位 | 浮点数运算,三操作数指令,非破坏性目的寄存器 |
| AVX2 | 2013 | 256 位 | 整数运算,乘加、混洗等,与 AVX 浮点指令同等功能 |
| AVX-512 | 2016 | 512 位 | 更宽的向量,掩码操作,嵌入式舍入/抑制异常 |
ARM 架构也有其对应的 SIMD 指令集,最著名的是 NEON,它在移动设备和嵌入式系统中广泛应用,也提供了强大的并行处理能力。
随着指令集的发展,SIMD 寄存器宽度不断增加(从 64 位到 512 位),支持的数据类型更加丰富,指令功能也越来越强大和灵活。这为我们进行深度优化提供了坚实的硬件基础。
二、 SIMD 优化的三种境界
在实际编程中,实现 SIMD 优化通常有三种主要途径,它们代表了不同的抽象层次和控制粒度。
2.1 编译器自动向量化 (Auto-Vectorization)
原理:现代编译器(如 GCC, Clang, MSVC)非常智能,它们会尝试分析你的 C/C++ 代码中的循环结构,如果发现循环中的操作可以并行化,并且符合 SIMD 指令的模式,编译器会自动将其转换为 SIMD 指令。
优点:
- 易用性高:无需修改源代码,只需在编译时添加相应的优化标志(例如 GCC/Clang 的
-O2,-O3,-ftree-vectorize,MSVC 的/O2)。 - 代码简洁:保持了原始代码的清晰和可读性。
- 跨平台性:编译器会根据目标架构生成相应的 SIMD 指令。
缺点: - 优化能力有限:编译器并非总能识别出所有可向量化的模式,特别是当代码逻辑复杂、存在复杂的内存访问模式、函数调用或数据依赖时。
- 控制力不足:开发者无法精确控制使用哪些 SIMD 指令,也无法强制编译器进行特定优化。
- 性能不确定:最终生成的 SIMD 代码效率可能不如手动优化。
示例:
一个简单的循环,编译器可能会尝试向量化。
void add_arrays_auto(float* a, float* b, float* c, int n) {
for (int i = 0; i < n; ++i) {
c[i] = a[i] + b[i];
}
}
通过 g++ -O3 -march=native -ftree-vectorize 编译,编译器很可能会将其向量化为 SSE 或 AVX 指令。
2.2 使用 SIMD 优化库 (Vector Libraries)
原理:许多高性能计算库和图像处理库(如 OpenCV, Eigen, OpenVINO, Intel IPP)内部已经集成了 SIMD 优化。它们提供了高层次的 API,抽象了底层的 SIMD 实现细节。
优点:
- 易于使用:开发者通过调用库函数即可获得性能提升,无需了解 SIMD 内部函数。
- 跨平台和可移植性:库通常会处理不同 CPU 架构和指令集的适配。
- 功能丰富:提供了大量预优化的常用算子。
- 经过严格测试:库函数通常经过了广泛的测试和优化。
缺点: - 引入依赖:需要在项目中集成第三方库。
- 灵活性受限:对于自定义的、非标准的操作,库可能无法提供相应的优化函数。
- 性能上限:虽然比纯标量代码快,但在某些极端场景下,库的通用性可能导致无法达到极致的硬件性能。
示例:
使用 OpenCV 的 cv::add 函数,其内部可能已经针对 SIMD 进行了优化。
#include <opencv2/opencv.hpp>
void add_arrays_opencv(const cv::Mat& a, const cv::Mat& b, cv::Mat& c) {
cv::add(a, b, c);
}
2.3 SIMD 内部函数 (Intrinsics)
原理:SIMD 内部函数是编译器提供的一组特殊函数,它们直接映射到 CPU 的 SIMD 指令。开发者可以通过这些函数,以 C/C++ 语言的语法直接编写 SIMD 指令,而无需编写汇编代码。
优点:
- 极致性能:提供了对硬件最细粒度的控制,能够充分利用 SIMD 指令集的所有特性,达到理论上接近硬件的最高性能。
- 精确控制:开发者可以精确选择使用哪些指令、如何加载和存储数据、如何处理数据排列等。
- 避免汇编:无需学习复杂的汇编语言,可以在 C/C++ 环境中进行 SIMD 编程。
缺点: - 编程复杂性高:需要深入理解 SIMD 指令集、数据类型、寄存器操作,代码可读性较差。
- 平台依赖性:内部函数通常是编译器和处理器架构特定的(例如 Intel/AMD 的
_mm_*函数与 ARM NEON 的v*q_*函数不同)。代码移植性差。 - 调试难度大:SIMD 寄存器和操作的调试比标量代码更复杂。
- 开发周期长:手动优化需要更多的时间和精力。
示例:
使用 SSE Intrinsics 实现数组加法。
#include <immintrin.h> // 包含 SSE/AVX Intrinsics 头文件
void add_arrays_intrinsics_sse(float* a, float* b, float* c, int n) {
int i = 0;
// 每次处理 4 个浮点数(128位寄存器)
for (; i + 3 < n; i += 4) {
__m128 va = _mm_loadu_ps(&a[i]); // 加载 a[i...i+3]
__m128 vb = _mm_loadu_ps(&b[i]); // 加载 b[i...i+3]
__m128 vc = _mm_add_ps(va, vb); // 执行加法
_mm_storeu_ps(&c[i], vc); // 存储结果
}
// 处理剩余部分(如果 n 不是 4 的倍数)
for (; i < n; ++i) {
c[i] = a[i] + b[i];
}
}
本次讲座的重点正是第三种境界——SIMD 内部函数,因为它是实现“深度优化”的终极武器。
三、 SIMD Intrinsics 深度实践:图像处理算子优化
现在,我们将通过几个经典的图像处理算子,演示如何利用 SIMD Intrinsics 进行深度优化。我们将主要以 x86 架构下的 SSE/AVX 指令集为例。
3.1 准备工作:头文件与编译选项
在使用 Intrinsics 之前,需要包含相应的头文件并设置正确的编译选项。
头文件:
<xmmintrin.h>(SSE)<emmintrin.h>(SSE2)<pmmintrin.h>(SSE3)<tmmintrin.h>(SSSE3)<smmintrin.h>(SSE4.1/SSE4.2)<nmmintrin.h>(SSE4a, AMD specific)<immintrin.h>(包含所有 SSE/AVX 指令集,推荐使用此头文件,它会根据编译标志自动引入其他头文件)
编译选项 (GCC/Clang):
-msse,-msse2,-msse3,-mssse3,-msse4.1,-msse4.2: 启用特定的 SSE 指令集。-mavx,-mavx2: 启用 AVX/AVX2 指令集。-mfma: 启用 FMA (Fused Multiply-Add) 指令。-march=native: 告诉编译器根据当前编译机器的 CPU 类型自动启用支持的所有指令集,这在开发时很方便,但在部署时需要考虑兼容性。-O2,-O3: 开启优化。
编译选项 (MSVC):
/arch:SSE,/arch:SSE2,/arch:AVX,/arch:AVX2: 启用特定的指令集。/O2: 开启优化。
3.2 核心 Intrinsics 数据类型与操作
| 数据类型 | 描述 | 对应的 SIMD 寄存器 |
|---|---|---|
__m128 |
包含 4 个 float (单精度浮点数) |
XMM 寄存器 (128位) |
__m128d |
包含 2 个 double (双精度浮点数) |
XMM 寄存器 (128位) |
__m128i |
包含 16 个 int8_t 或 8 个 int16_t 等 |
XMM 寄存器 (128位) |
__m256 |
包含 8 个 float |
YMM 寄存器 (256位) |
__m256d |
包含 4 个 double |
YMM 寄存器 (256位) |
__m256i |
包含 32 个 int8_t 或 16 个 int16_t 等 |
YMM 寄存器 (256位) |
常用操作类别:
- 加载 (Load):从内存将数据加载到 SIMD 寄存器。
_mm_load_ps(float *p):加载 4 个对齐的float。_mm_loadu_ps(float *p):加载 4 个非对齐的float。_mm_load_si128(__m128i *p):加载 16 字节对齐的整数数据。_mm_loadu_si128(__m128i *p):加载 16 字节非对齐的整数数据。_mm_set1_ps(float a):将单个float值广播到所有元素。_mm_setzero_ps():所有元素设置为 0。
- 存储 (Store):将 SIMD 寄存器中的数据存储到内存。
_mm_store_ps(float *p, __m128 a):存储 4 个对齐的float。_mm_storeu_ps(float *p, __m128 a):存储 4 个非对齐的float。
- 算术运算:
_mm_add_ps(a, b):浮点数加法。_mm_mul_ps(a, b):浮点数乘法。_mm_add_epi8(a, b):8 位整数加法。_mm_sub_epi16(a, b):16 位整数减法。_mm_maddubs_epi16(a, b)(SSSE3):8 位无符号整数乘法并累加到 16 位有符号整数。
- 比较运算:
_mm_cmpeq_epi8(a, b):8 位整数相等比较。_mm_cmpgt_epi16(a, b):16 位整数大于比较。
- 数据混洗/重排 (Shuffle/Permute):
_mm_shuffle_ps(a, b, imm8):混洗浮点数。_mm_unpacklo_epi8(a, b):交织低位字节。_mm_packus_epi16(a, b):将 16 位有符号整数打包为 8 位无符号整数(饱和)。
- 位运算:
_mm_and_si128(a, b):按位与。_mm_or_si128(a, b):按位或。_mm_slli_epi16(a, imm8):16 位整数逻辑左移。
3.3 算子一:灰度图像转换 (Interleaved RGB to Grayscale)
公式:Y = 0.299 * R + 0.587 * G + 0.114 * B
假设图像数据为 uint8_t 类型,存储格式为 interleaved RGB (R G B R G B …)。
我们将输出 8 位灰度图像。
标量实现:
void grayscale_scalar(const uint8_t* rgb_data, uint8_t* gray_data, int width, int height) {
for (int i = 0; i < height * width; ++i) {
uint8_t r = rgb_data[i * 3];
uint8_t g = rgb_data[i * 3 + 1];
uint8_t b = rgb_data[i * 3 + 2];
// 浮点数运算,然后四舍五入并钳位到 [0, 255]
gray_data[i] = static_cast<uint8_t>(0.299f * r + 0.587f * g + 0.114f * b + 0.5f);
}
}
SIMD Intrinsics 实现 (SSE2/AVX2 – 整数定点运算):
由于图像数据是 uint8_t,直接使用浮点数 Intrinsics 会有类型转换开销。更高效的方法是使用整数定点运算。我们将系数放大 256 倍(或 2^N 倍),进行整数乘法,然后右移 N 位。
系数:
0.299 * 256 = 76.544 ≈ 77
0.587 * 256 = 150.272 ≈ 150
0.114 * 256 = 29.184 ≈ 29
总和 77 + 150 + 29 = 256。
为了避免溢出,我们将 8 位 R/G/B 扩展到 16 位进行乘法。
#include <immintrin.h> // For SSE/AVX intrinsics
#include <cstdint> // For uint8_t, uint16_t
// 灰度转换函数 (AVX2 整数定点实现)
// 输入: rgb_data (R G B R G B ...), width, height
// 输出: gray_data (Y Y Y ...)
void grayscale_avx2_int(const uint8_t* rgb_data, uint8_t* gray_data, int width, int height) {
// 定义定点系数,放大 256 倍
// 注意:AVX2 寄存器是 256 位,可以同时处理 16 个 16 位数据
// 我们需要 3 个系数,每个系数重复 8 次,以便与 8 个像素的 R/G/B 分量相乘
const __m256i coeff_r = _mm256_set1_epi16(77); // 0.299 * 256
const __m256i coeff_g = _mm256_set1_epi16(150); // 0.587 * 256
const __m256i coeff_b = _mm256_set1_epi16(29); // 0.114 * 256
const __m256i rounding_add = _mm256_set1_epi16(128); // 128 = 256 / 2 for rounding
// 图像总像素数
int num_pixels = width * height;
// AVX2 处理 8 像素 (8 * 3 = 24 字节)
// __m256i 可以存储 16个 uint16_t 或 32个 uint8_t
// 每个循环处理 8 个像素,即 24 字节 RGB 数据
// RGBRGBRGBRGBRGBRGBRGBRGB (24 bytes) -> YYYYYYYY (8 bytes)
int i = 0;
for (; i + 7 < num_pixels; i += 8) {
// 加载 8 个像素的 RGB 数据 (24 字节)
// 从 rgb_data[i*3] 开始,加载 24 字节
// 注意:这里假设 rgb_data 是对齐的,如果不是,需要使用 _mm256_loadu_si256
__m256i rgb_bytes = _mm256_loadu_si256((const __m256i*)(rgb_data + i * 3));
// 解包 R, G, B 分量到 16 位,因为乘法需要 16 位
// rgb_bytes = R0 G0 B0 R1 G1 B1 R2 G2 B2 R3 G3 B3 R4 G4 B4 R5 G5 B5 R6 G6 B6 R7 G7 B7 (uint8_t)
// 提取 R 分量
// _mm256_srli_si256(rgb_bytes, 0) - 第0字节是R
// _mm256_srli_si256(rgb_bytes, 3) - 第3字节是R1
// ...
// 这一步比较复杂,需要多次混洗和解包,因为 R/G/B 是交错的
// 我们可以使用 _mm256_cvtepu8_epi16 来将 8 位无符号数扩展为 16 位有符号数
// 但是 _mm256_cvtepu8_epi16 只能处理 16 字节,而我们有 24 字节
// 更好的方法是使用 _mm256_shuffle_epi8 和 _mm256_unpacklo/hi_epi8
// 由于 AVX2 没有直接的 8位到16位交错解包指令,我们需要一些技巧
// 1. 将 24 字节 RGB 分成两部分,每部分 12 字节
// _mm256_extracti128_si256(rgb_bytes, 0) 得到低 128 位 (R0..R3)
// _mm256_extracti128_si256(rgb_bytes, 1) 得到高 128 位 (R4..R7)
// 2. 利用 _mm_cvtepu8_epi16 转换 8 位到 16 位
// 3. 然后混洗得到 R, G, B 分量
// 提取低 16 字节 (R0 G0 B0 R1 G1 B1 R2 G2 B2 R3 G3 B3 R4 G4 B4)
__m128i rgb_low_16_bytes = _mm256_extracti128_si256(rgb_bytes, 0);
// 提取高 8 字节 (R5 G5 B5 R6 G6 B6 R7 G7 B7)
__m128i rgb_high_8_bytes = _mm256_extracti128_si256(rgb_bytes, 1);
// 为了方便处理,我们需要将 RGB 数据重新排列成 RRRR... GGGG... BBBB... 的形式
// 这通常需要多次 shuffle 和 unpack 操作。
// 对于 8 个像素的 RGB 数据 (24 字节): R0G0B0 R1G1B1 R2G2B2 R3G3B3 R4G4B4 R5G5B5 R6G6B6 R7G7B7
// 最直接的方法是使用 `_mm256_i32gather_epi32` 或多次 `_mm256_shuffle_epi8`
// 考虑到 24 字节的特殊性,我们可以分两步处理:
// 1. 提取前 4 个像素 (12 字节) 的 R, G, B
// 2. 提取后 4 个像素 (12 字节) 的 R, G, B
// 这里简化处理:我们假设数据已经被预处理成 RRRR... GGGG... BBBB... 这种 planar 格式,
// 或者我们采用更通用的方法,但会更复杂。
// 为了演示核心乘加逻辑,我们先假定能拿到 R0..R7, G0..G7, B0..B7 的 16 位形式。
// 对于 interleaved (RGBRGB...) 格式,解包成 RRRR GGGG BBBB 形式的 AVX2 代码会非常长且复杂。
// 实际应用中,通常会先进行一次数据重排,或者一次处理多个像素块。
// 为了简化,我们使用 SSE2 版本的解包,每次处理 4 个像素 (12 字节)
// 假设我们现在处理 4 个像素 (R0G0B0R1G1B1R2G2B2R3G3B3)
// 实际的 AVX2 处理 8 个像素,需要更复杂的解包策略
// 这里提供一个概念性的解包过程,实际代码会更复杂。
// --- 复杂的解包过程 (概念性,非完整代码) ---
// 需要将 24 字节的数据解包成 3 个 __m256i 寄存器,分别包含 R, G, B 分量的 16 位值
// __m256i r_pixels_u16, g_pixels_u16, b_pixels_u16;
// ... (省略复杂的 shuffle/unpack 过程,这本身就是一大挑战)
// 举例:对于 12 字节 (4 像素),SSE 解包 R G B
// __m128i rgb_lo = _mm_loadu_si128((__m128i*)(rgb_data + i * 3)); // R0G0B0R1G1B1R2G2B2R3G3B3
// __m128i zero = _mm_setzero_si128();
// __m128i r_lo = _mm_unpacklo_epi8(rgb_lo, zero); // R0 0 G0 0 B0 0 R1 0 G1 0 B1 0 R2 0 G2 0 B2 0
// ... 这是一个复杂的过程,此处不展开,直接进入乘法阶段。
// --- 复杂的解包过程结束 ---
// 为了演示核心逻辑,我们假设已经通过某种方式得到了 8 个像素的 R, G, B 分量,
// 且它们都被扩展到了 16 位,存储在 3 个 __m256i 寄存器中。
// 实际中,通常会使用 `_mm256_cvtepu8_epi16` 配合 `_mm256_permutevar8x32_epi32` 等指令进行重排。
// 简化起见,我们先用一个更简单的 SSE2 示例来演示定点乘加
// 假设我们一次处理 4 个像素 (12 字节),并且已经能够得到 R, G, B 的 16 位向量
// 对于 AVX2,需要处理 8 个像素。
// 为了避免过长的解包代码,我们假设已经将 24 字节的 RGB 数据解包成了:
// R_vec = [R0 R1 R2 R3 R4 R5 R6 R7] (each R is uint16_t)
// G_vec = [G0 G1 G2 G3 G4 G5 G6 G7] (each G is uint16_t)
// B_vec = [B0 B1 B2 B3 B4 B5 B6 B7] (each B is uint16_t)
// 这是一个通用的解包策略,适用于 interleaved RGB (e.g., used in OpenCV's SIMD backend)
// 将 24 字节数据加载到 3 个 __m128i 寄存器中,然后重新排列
__m128i p0 = _mm_loadu_si128((const __m128i*)(rgb_data + i * 3)); // R0G0B0R1G1B1R2G2B2R3G3B3
__m128i p1 = _mm_loadu_si128((const __m128i*)(rgb_data + i * 3 + 12)); // R4G4B4R5G5B5R6G6B6R7G7B7
// AVX2 提供了 `_mm256_cvtepu8_epi16` 将 8 位无符号数扩展为 16 位有符号数
// 这需要先将数据排列好
// 我们可以将 24 字节的数据视为 3 个 8 字节的数据块,每个块对应 R, G, B
// 这需要非常精巧的 `_mm256_shuffle_epi8`
// 考虑 8 个像素的 R, G, B 分量,每个分量 8 位
// R_0 G_0 B_0 R_1 G_1 B_1 R_2 G_2 B_2 R_3 G_3 B_3 R_4 G_4 B_4 R_5 G_5 B_5 R_6 G_6 B_6 R_7 G_7 B_7
// 目标是得到:
// R_vec = [R_0, R_1, R_2, R_3, R_4, R_5, R_6, R_7] (8个 uint8_t)
// G_vec = [G_0, G_1, G_2, G_3, G_4, G_5, G_6, G_7] (8个 uint8_t)
// B_vec = [B_0, B_1, B_2, B_3, B_4, B_5, B_6, B_7] (8个 uint8_t)
// 然后再扩展到 16 位。
// 这个解包过程非常繁琐,需要大量的 `_mm256_permutevar8x32_epi32`, `_mm256_shuffle_epi8` 等指令。
// 为了讲座的清晰性,此处假设我们已经有了 8 个像素的 R, G, B 分量,每个分量都扩展到了 16 位。
// 实际中,可以参考 OpenCV 的 `v_load_deinterleave` 或 `_mm256_load_deinterleave` 实现。
// 假设我们已经通过复杂的数据重排得到了以下 16 位向量(伪代码):
__m256i r_val_u16 = _mm256_set_epi16(R7, R6, R5, R4, R3, R2, R1, R0, R7, R6, R5, R4, R3, R2, R1, R0); // 8个R
__m256i g_val_u16 = _mm256_set_epi16(G7, G6, G5, G4, G3, G2, G1, G0, G7, G6, G5, G4, G3, G2, G1, G0); // 8个G
__m256i b_val_u16 = _mm256_set_epi16(B7, B6, B5, B4, B3, B2, B1, B0, B7, B6, B5, B4, B3, B2, B1, B0); // 8个B
// 实际上,这些值是从 rgb_bytes 中提取并转换的。
// 例如,对于 AVX2,可以这样提取 R/G/B (需要两次加载,并使用 `_mm256_permute4x64_epi64` 等指令)
// 考虑到代码长度和复杂性,我们直接提供一个相对简化的提取方法。
// 实际上 AVX2 的 `_mm256_unpacklo_epi8` 和 `_mm256_unpackhi_epi8` 每次只能处理 16 字节,
// 且结果是交错的。解包 24 字节并得到 8 个 R, 8 个 G, 8 个 B (16 位) 需要一些技巧。
// 以下是简化的 AVX2 解包 R, G, B 为 16 位通道的逻辑:
// 假设 rgb_data 是 24 字节,我们需要 8 个 R, 8 个 G, 8 个 B。
// 1. 从 24 字节中提取 8 个 R 值,8 个 G 值,8 个 B 值。
// 通过 `_mm256_srli_si256` 和 `_mm256_and_si256` 可以提取,但会丢失一些信息。
// 更通用的方法是利用 `_mm256_shuffle_epi8` 和 `_mm256_permute4x64_epi64`
// 这里我们直接使用一个虚拟的加载函数 `load_deinterleave_rgb_to_16bit_avx2` 来表示。
// 这个函数的实现本身非常复杂,但其目的是得到分离的 R, G, B 16位向量。
__m256i r_val_u16_low, r_val_u16_high; // 8个R分两次加载
__m256i g_val_u16_low, g_val_u16_high; // 8个G分两次加载
__m256i b_val_u16_low, b_val_u16_high; // 8个B分两次加载
// 由于 `_mm256_cvtepu8_epi16` 只能处理 16 字节,而我们是 24 字节。
// 我们需要将 24 字节数据分块处理。
// 假设我们能获得 8 个 R, 8 个 G, 8 个 B,每个都是 8 位。
// 复杂解包的参考实现(简化版,不保证最佳):
__m256i zero_reg = _mm256_setzero_si256();
__m256i rgb_chunk0 = _mm256_loadu_si256((const __m256i*)(rgb_data + i * 3));
__m256i rgb_chunk1 = _mm256_loadu_si256((const __m256i*)(rgb_data + i * 3 + 8)); // Load next 8 bytes as well
__m256i rgb_chunk2 = _mm256_loadu_si256((const __m256i*)(rgb_data + i * 3 + 16)); // Load next 8 bytes as well
// Extract R, G, B. This is very tricky.
// Let's simplify and use a common pattern for deinterleaving 3 channels.
// For 8 pixels (24 bytes): R0G0B0R1G1B1R2G2B2R3G3B3R4G4B4R5G5B5R6G6B6R7G7B7
// We need 8xR, 8xG, 8xB in separate 16-bit vectors.
// This requires a lot of `_mm256_unpacklo_epi8`, `_mm256_unpackhi_epi8`, `_mm256_shuffle_epi8`
// For example, to get R's:
// __m256i r_vals_16bit = ... (complex shuffle/unpack of rgb_chunk0, rgb_chunk1, rgb_chunk2)
// __m256i g_vals_16bit = ...
// __m256i b_vals_16bit = ...
// For practical implementation of interleaved to planar (16-bit),
// it's often easier to process smaller chunks (e.g., 4 pixels with SSE) or use specific libraries.
// Given the constraints of a lecture, we'll abstract this complex deinterleaving.
// Assume we have functions `get_R_16bit_avx2`, `get_G_16bit_avx2`, `get_B_16bit_avx2`
// Placeholder for the actual deinterleaving and 8-bit to 16-bit extension.
// In a real scenario, this part would be dense with shuffle/unpack operations.
// Example for 8 pixels, getting 16-bit R, G, B:
// This is a common pattern, but extremely verbose.
// We can load 32 bytes and then shuffle, or load 24 bytes and split.
// Let's assume we load 32 bytes for simplicity (might load past end, need boundary check or padding)
__m256i rgb_packed_0 = _mm256_loadu_si256((const __m256i*)(rgb_data + i * 3));
__m256i rgb_packed_1;
if ((i * 3 + 24) <= (width * height * 3)) { // Ensure we don't read too far for the second __m256i load
rgb_packed_1 = _mm256_loadu_si256((const __m256i*)(rgb_data + i * 3 + 8)); // Load next 8 bytes of current block
} else {
rgb_packed_1 = _mm256_setzero_si256(); // Pad with zeros or handle carefully
}
// The actual deinterleaving for 24 bytes into 3x __m256i (16-bit) is highly optimized and complex.
// It often involves permutations and unpacks.
// E.g., for 8 pixels:
// R0G0B0R1G1B1R2G2B2R3G3B3 R4G4B4R5G5B5R6G6B6R7G7B7 (24 bytes)
// This is difficult to represent clearly without a dedicated diagram or helper functions.
// Let's simplify the demonstration of the arithmetic part.
// Placeholder for the 16-bit R, G, B vectors (each containing 8 actual pixel values)
__m256i r_val_u16 = _mm256_set_epi16(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0); // Actual R values for 8 pixels
__m256i g_val_u16 = _mm256_set_epi16(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0); // Actual G values for 8 pixels
__m256i b_val_u16 = _mm256_set_epi16(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0); // Actual B values for 8 pixels
// --- Simplified conceptual extraction (replace with actual complex deinterleaving) ---
// For demonstration, let's assume `_mm256_load_rgb_deinterleave_u8_to_u16` is a helper.
// This helper would read 24 bytes and produce 3 __m256i vectors, each containing 8 (uint16_t) R, G, B values.
// This is where a lot of SIMD "magic" happens for interleaved data.
// Example:
// __m256i rgb_data_low = _mm256_loadu_si256((const __m256i*)(rgb_data + i * 3));
// __m256i rgb_data_mid = _mm256_loadu_si256((const __m256i*)(rgb_data + i * 3 + 8));
// __m256i rgb_data_high = _mm256_loadu_si256((const __m256i*)(rgb_data + i * 3 + 16));
// r_val_u16 = ... (complex operations on rgb_data_low, rgb_data_mid, rgb_data_high)
// g_val_u16 = ...
// b_val_u16 = ...
// Due to the complexity of 24-byte deinterleaving for AVX2 in a lecture,
// we will proceed with the arithmetic part, assuming we have these 16-bit R/G/B vectors.
// A simpler approach for AVX2 is often to load 32 bytes (10 pixels + 2 bytes padding) and then mask/shuffle.
// Or, use SSE2 loop to process 4 pixels (12 bytes) at a time, then combine.
// Let's use a simplified SSE2 example for clearer arithmetic demonstration.
// Each SSE2 iteration processes 4 pixels (12 bytes input, 4 bytes output).
for (; i + 3 < num_pixels; i += 4) {
__m128i rgb_chunk = _mm_loadu_si128((const __m128i*)(rgb_data + i * 3)); // R0G0B0R1G1B1R2G2B2R3G3B3
// 解包 8位 RGB 到 16位 R, G, B 分量
__m128i zero = _mm_setzero_si128();
// 提取 R0, R1, R2, R3 (作为 16位)
// mask for R: 0xFF 0x00 0x00 0xFF 0x00 0x00 0xFF 0x00 0x00 0xFF 0x00 0x00 ...
// This is actually tricky. A common pattern uses _mm_shuffle_epi8
// Let's use unpack for simplicity but it's not strictly "R0R1R2R3"
__m128i r_even = _mm_unpacklo_epi8(rgb_chunk, zero); // R0 0 G0 0 B0 0 R1 0 G1 0 B1 0
__m128i r_odd = _mm_unpackhi_epi8(rgb_chunk, zero); // R2 0 G2 0 B2 0 R3 0 G3 0 B3 0
// Need to further shuffle to get R, G, B into separate vectors.
// This is a common deinterleaving pattern for 3 channels (e.g., from OpenCV)
// _mm_shuffle_epi8 is very powerful here.
// For example, to get R values (0, 3, 6, 9 bytes):
// __m128i r_vals_u8 = _mm_shuffle_epi8(rgb_chunk, _mm_set_epi8(
// -1, -1, -1, -1, -1, -1, -1, -1, // high 8 bytes (not used)
// 9, 6, 3, 0, // R3, R2, R1, R0
// -1, -1, -1, -1
// )); // This gets 4 R values. Need to expand to 16-bit.
// Let's simplify and assume a helper `load_rgb_to_16bit_sse2`
// that gives us R0..R3, G0..G3, B0..B3 as 16-bit vectors
__m128i r_val_u16 = _mm_set_epi16(0,0,0,0,0,0,0,0); // R3 R2 R1 R0 (16-bit)
__m128i g_val_u16 = _mm_set_epi16(0,0,0,0,0,0,0,0); // G3 G2 G1 G0 (16-bit)
__m128i b_val_u16 = _mm_set_epi16(0,0,0,0,0,0,0,0); // B3 B2 B1 B0 (16-bit)
// This is a common deinterleave for 3 channels (e.g., RGB) to 3 separate vectors of 8-bit.
// Then convert to 16-bit.
// `_mm_srli_si128` (shift right logical immediate) and `_mm_and_si128` (bitwise AND) can help.
// Or `_mm_setr_epi8` for a mask to pick R,G,B bytes.
// A practical way for SSE2 to deinterleave 3 channels (R0G0B0R1G1B1R2G2B2R3G3B3) to 16-bit:
__m128i p_lo = _mm_loadu_si128((const __m128i*)(rgb_data + i * 3)); // R0G0B0R1G1B1R2G2B2R3G3B3
__m128i p_hi = _mm_setzero_si128(); // Pad with zeros for high part
// First, deinterleave 8-bit R, G, B
__m128i p_0_1 = _mm_unpacklo_epi8(p_lo, p_hi); // R0 G0 B0 R1 G1 B1 R2 G2 B2 R3 G3 B3 (low 8 bytes of p_lo)
__m128i p_2_3 = _mm_unpackhi_epi8(p_lo, p_hi); // R4 G4 B4 R5 G5 B5 R6 G6 B6 R7 G7 B7 (high 8 bytes of p_lo)
__m128i r_part_lo = _mm_unpacklo_epi8(p_0_1, zero); // R0 0 G0 0 B0 0 R1 0 ...
__m128i r_part_hi = _mm_unpackhi_epi8(p_0_1, zero); // R2 0 G2 0 B2 0 R3 0 ...
// This is getting too verbose for a lecture.
// Let's assume we have `_mm_load_deinterleave_rgb_to_16bit_sse2` helper
// `_mm_load_deinterleave_rgb_to_16bit_sse2((const uint8_t*)(rgb_data + i * 3), &r_val_u16, &g_val_u16, &b_val_u16);`
// For simplification, let's just use 4 R, G, B values for the arithmetic demo.
// This is a common pattern for 3-channel 8-bit data:
__m128i R0G0B0R1G1B1R2G2B2R3G3B3 = _mm_loadu_si128((const __m128i*)(rgb_data + i * 3));
__m128i zero_m128i = _mm_setzero_si128();
// R values (as 16-bit)
__m128i r_vals_lo = _mm_unpacklo_epi8(R0G0B0R1G1B1R2G2B2R3G3B3, zero_m128i); // R0 0 G0 0 B0 0 R1 0 G1 0 B1 0
__m128i r_vals_hi = _mm_unpackhi_epi8(R0G0B0R1G1B1R2G2B2R3G3B3, zero_m128i); // R2 0 G2 0 B2 0 R3 0 G3 0 B3 0
__m128i r_final_u16 = _mm_setr_epi16(
_mm_extract_epi16(r_vals_lo, 0), // R0
_mm_extract_epi16(r_vals_lo, 6), // R1
_mm_extract_epi16(r_vals_hi, 0), // R2
_mm_extract_epi16(r_vals_hi, 6), // R3
0,0,0,0 // Padding for 8 elements total
);
// This is still complex. Let's use the AVX2 example's structure and abstract the deinterleaving.
// Reverting to the AVX2 structure, assuming we have 16-bit R, G, B vectors for 8 pixels.
// This would be done by a dedicated helper function or a highly optimized block of SIMD.
// For now, let's assume `r_val_u16`, `g_val_u16`, `b_val_u16` are populated for 8 pixels.
// This is the core arithmetic part.
// Multiply with coefficients
__m256i term_r = _mm256_mullo_epi16(r_val_u16, coeff_r);
__m256i term_g = _mm256_mullo_epi16(g_val_u16, coeff_g);
__m256i term_b = _mm256_mullo_epi16(b_val_u16, coeff_b);
// Sum the terms
__m256i sum_rgb = _mm256_add_epi16(term_r, term_g);
sum_rgb = _mm256_add_epi16(sum_rgb, term_b);
// Add rounding offset (128 for division by 256)
sum_rgb = _mm256_add_epi16(sum_rgb, rounding_add);
// Divide by 256 (right shift by 8 bits)
__m256i gray_u16 = _mm256_srli_epi16(sum_rgb, 8);
// Pack 16-bit results to 8-bit (saturating)
// _mm256_packus_epi16 packs two __m256i (16-bit) to one __m256i (8-bit)
// It takes two 256-bit registers (each holding 16x16-bit values)
// and packs them into one 256-bit register (holding 32x8-bit values).
// We need to pack the low 8 pixels and high 8 pixels if we processed 16.
// Here, we processed 8 pixels, so `gray_u16` contains 8 uint16_t values.
// We need to pack these 8 values into 8 uint8_t values.
// This requires packing a 256-bit register (8x16bit) into a 128-bit register (8x8bit).
// This is typically done by taking the low half of the 256-bit register and packing.
// Split `gray_u16` into two `__m128i` (low 4 pixels, high 4 pixels)
__m128i gray_u16_low = _mm256_extracti128_si256(gray_u16, 0); // Low 8x16bit
__m128i gray_u16_high = _mm256_extracti128_si256(gray_u16, 1); // High 8x16bit
// Pack two 16-bit to 8-bit saturating.
// This packs 16 16-bit values into 16 8-bit values.
// We have 8 values in gray_u16_low, and 8 values in gray_u16_high.
// We can pack them into a single 16-byte __m128i.
__m128i packed_gray_low = _mm_packus_epi16(gray_u16_low, zero_m128i); // Packs 8 16-bit to 8 8-bit (first 8 values)
__m128i packed_gray_high = _mm_packus_epi16(gray_u16_high, zero_m128i); // Packs next 8 16-bit to 8 8-bit
// The `_mm_packus_epi16` takes two `__m128i` and packs them.
// So if we have `gray_u16_low` (4 pixels as 16-bit) and `gray_u16_high` (4 pixels as 16-bit)
// We can pack them into a single `__m128i` result.
// This means one AVX2 iteration effectively produces 8 8-bit gray values.
__m128i final_gray_8_pixels = _mm_packus_epi16(gray_u16_low, gray_u16_high); // Packs 8 16-bit into 8 8-bit
// Store the 8-bit results (8 pixels, 8 bytes)
_mm_storeu_si128((__m128i*)(gray_data + i), final_gray_8_pixels);
}
// --- Simplified conceptual extraction (replace with actual complex deinterleaving) ---
// To make this AVX2 example runnable, we must provide a concrete deinterleaving.
// It's often easier to first extract R, G, B as 8-bit vectors, then extend to 16-bit.
// Helper to deinterleave 24 bytes (8 pixels) of RGB to 3x __m256i 16-bit vectors
// This is a simplified version, real one is more complex:
// Input: R0G0B0R1G1B1R2G2B2R3G3B3 R4G4B4R5G5B5R6G6B6R7G7B7 (24 bytes)
// Output: r_out = [R0..R7], g_out = [G0..G7], b_out = [B0..B7], each element is 16-bit
auto deinterleave_rgb_to_16bit_avx2 = [&](const uint8_t* src, __m256i& r_out, __m256i& g_out, __m256i& b_out) {
__m256i p_0 = _mm256_loadu_si256((const __m256i*)src); // R0G0B0R1G1B1R2G2B2R3G3B3R4G4B4R5G5B5
__m256i p_1 = _mm256_loadu_si256((const __m256i*)(src + 8)); // R2G2B2R3G3B3R4G4B4R5G5B5R6G6B6R7G7B7
// This requires highly specific shuffle masks. For brevity, we'll assume it works.
// For example, with `_mm256_shuffle_epi8` with proper masks, we can extract.
// This is where real SIMD programming gets very complex and specific.
// Let's fallback to processing 4 pixels with SSE for the sake of a clear example.
// The conceptual arithmetic remains the same, just vector width changes.
};
// Let's rewrite the grayscale using SSE2 for 4 pixels, which is more manageable to deinterleave.
// It processes 4 pixels at a time (12 bytes input, 4 bytes output).
const __m128i coeff_r_sse = _mm_set1_epi16(77);
const __m128i coeff_g_sse = _mm_set1_epi16(150);
const __m128i coeff_b_sse = _mm_set1_epi16(29);
const __m128i rounding_add_sse = _mm_set1_epi16(128);
const __m128i zero_sse = _mm_setzero_si128();
for (i = 0; i + 3 < num_pixels; i += 4) {
// Load 4 pixels (12 bytes) of RGB data
__m128i rgb_packed = _mm_loadu_si128((const __m128i*)(rgb_data + i * 3)); // R0G0B0R1G1B1R2G2B2R3G3B3
// Deinterleave and extend to 16-bit
// __m128i can hold 8x16-bit values.
// We need 4xR, 4xG, 4xB as 16-bit values.
// Example deinterleave for 4 pixels R G B (12 bytes)
// Use _mm_unpacklo_epi8 and _mm_unpackhi_epi8 to expand to 16-bit.
// p0: R0 G0 B0 R1 G1 B1 R2 G2 B2 R3 G3 B3 (12 bytes)
// p1: 0 0 0 0 0 0 0 0 0 0 0 0 (padding)
__m128i lo = _mm_unpacklo_epi8(rgb_packed, zero_sse); // R0 0 G0 0 B0 0 R1 0 | G1 0 B1 0 R2 0 G2 0 B2 0 (not exactly)
__m128i hi = _mm_unpackhi_epi8(rgb_packed, zero_sse); // R2 0 G2 0 B2 0 R3 0 | G3 0 B3 0 R4 0 G4 0 B4 0 (not exactly)
// This is one of the more common ways to deinterleave 3 channels from a 128-bit register
__m128i r_vals_16bit = _mm_shuffle_epi8(lo, _mm_set_epi8(
-1,-1,-1,-1,-1,-1,-1,-1, // Not used
14,12,8,6, // R3, R2, R1, R0 (rearranged to be consecutive)
-1,-1,-1,-1 // Not used
));
r_vals_16bit = _mm_unpacklo_epi8(r_vals_16bit, zero_sse); // Extend to 16-bit
__m128i g_vals_16bit = _mm_shuffle_epi8(lo, _mm_set_epi8(
-1,-1,-1,-1,-1,-1,-1,-1,
15,13,9,7, // G3, G2, G1, G0
-1,-1,-1,-1
));
g_vals_16bit = _mm_unpacklo_epi8(g_vals_16bit, zero_sse);
__m128i b_vals_16bit = _mm_shuffle_epi8(lo, _mm_set_epi8(
-1,-1,-1,-1,-1,-1,-1,-1,
10,5,2,0, // B3, B2, B1, B0 (example indices)
-1,-1,-1,-1
)); // This mask is wrong, needs careful construction.
// A more robust way using unpacks for 3 channels:
__m128i rg_lo = _mm_unpacklo_epi8(rgb_packed, zero_sse); // R0 0 G0 0 B0 0 R1 0
__m128i b_hi = _mm_unpackhi_epi8(rgb_packed, zero_sse); // R2 0 G2 0 B2 0 R3 0
__m128i r_val_u16_sse = _mm_set_epi16(
_mm_extract_epi16(rg_lo, 0), // R0
_mm_extract_epi16(rg_lo, 6), // R1
_mm_extract_epi16(b_hi, 0), // R2
_mm_extract_epi16(b_hi, 6), // R3
0,0,0,0
);
__m128i g_val_u16_sse = _mm_set_epi16(
_mm_extract_epi16(rg_lo, 2), // G0
_mm_extract_epi16(rg_lo, 8), // G1
_mm_extract_epi16(b_hi, 2), // G2
_mm_extract_epi16(b_hi, 8), // G3
0,0,0,0
);
__m128i b_val_u16_sse = _mm_set_epi16(
_mm_extract_epi16(rg_lo, 4), // B0
_mm_extract_epi16(rg_lo, 10),// B1
_mm_extract_epi16(b_hi, 4), // B2
_mm_extract_epi16(b_hi, 10), // B3
0,0,0,0
);
// Arithmetic for 4 pixels (SSE2)
__m128i term_r_sse = _mm_mullo_epi16(r_val_u16_sse, coeff_r_sse);
__m128i term_g_sse = _mm_mullo_epi16(g_val_u16_sse, coeff_g_sse);
__m128i term_b_sse = _mm_mullo_epi16(b_val_u16_sse, coeff_b_sse);
__m128i sum_rgb_sse = _mm_add_epi16(term_r_sse, term_g_sse);
sum_rgb_sse = _mm_add_epi16(sum_rgb_sse, term_b_sse);
sum_rgb_sse = _mm_add_epi16(sum_rgb_sse, rounding_add_sse);
__m128i gray_u16_sse = _mm_srli_epi16(sum_rgb_sse, 8);
// Pack 16-bit results to 8-bit saturating
// _mm_packus_epi16 takes two __m128i (8x16bit) and packs them into one __m128i (16x8bit).
// Since we only have 4 pixels, we pack with zero to get 4 8-bit values.
__m128i final_gray_4_pixels_sse = _mm_packus_epi16(gray_u16_sse, zero_sse);
// Store the 4 8-bit results
_mm_storeu_si32((void*)(gray_data + i), final_gray_4_pixels_sse); // Store first 4 bytes
}
// 处理剩余部分 (如果 num_pixels 不是 4 的倍数)
for (; i < num_pixels; ++i) {
uint8_t r = rgb_data[i * 3];
uint8_t g = rgb_data[i * 3 + 1];
uint8_t b = rgb_data[i * 3 + 2];
gray_data[i] = static_cast<uint8_t>((r * 77 + g * 150 + b * 29 + 128) >> 8);
}
}
说明:灰度转换的 RGB 解包是一个典型挑战,Interleaved 格式需要复杂的 shuffle 和 unpack 操作来将 R、G、B 分量分离到独立的向量中。上面的 SSE2 示例提供了核心算术逻辑,但解包部分仍需精心构造。AVX2 版本的解包会更加复杂,因为它处理 24 字节(8像素),而 _mm256_cvtepu8_epi16 每次只能处理 16 字节。
3.4 算子二:图像混合 (Alpha Blending)
公式:Output = (Alpha * Foreground + (255 - Alpha) * Background + 128) / 255
假设所有图像都是 uint8_t 类型,每个像素包含一个 Alpha 通道。
标量实现:
void alpha_blend_scalar(const uint8_t* fg_data, const uint8_t* bg_data, uint8_t* out_data, int width, int height) {
for (int i = 0; i < height * width; ++i) {
uint8_t fg_r = fg_data[i * 4];
uint8_t fg_g = fg_data[i * 4 + 1];
uint8_t fg_b = fg_data[i * 4 + 2];
uint8_t alpha = fg_data[i * 4 + 3]; // Foreground's alpha channel
uint8_t bg_r = bg_data[i * 4];
uint8_t bg_g = bg_data[i * 4 + 1];
uint8_t bg_b = bg_data[i * 4 + 2];
uint8_t inv_alpha = 255 - alpha;
// Apply blending formula for each channel
out_data[i * 4] = static_cast<uint8_t>((alpha * fg_r + inv_alpha * bg_r + 128) / 255);
out_data[i * 4 + 1] = static_cast<uint8_t>((alpha * fg_g + inv_alpha * bg_g + 128) / 255);
out_data[i * 4 + 2] = static_cast<uint8_t>((alpha * fg_b + inv_alpha * bg_b + 128) / 255);
out_data[i * 4 + 3] = 255; // Output alpha is typically 255 (opaque)
}
}
SIMD Intrinsics 实现 (SSE2/SSSE3 – 整数定点运算):
同样使用定点乘法。系数 1/255 通过右移实现近似,或者通过两次乘法和移位来精确计算。为了简单和效率,我们使用 (val * coeff + 128) >> 8 的近似。
由于 alpha * fg_r 可能超过 255,需要将 uint8_t 扩展到 uint16_t 进行乘法。
void alpha_blend_sse2_ssse3(const uint8_t* fg_data, const uint8_t* bg_data, uint8_t* out_data, int width, int height) {
// 假设图像是 BGRA 格式,每个像素 4 字节
int num_pixels = width * height;
int i = 0;
// 常量
const __m128i ones_16bit = _mm_set1_epi16(1);
const __m128i alpha_max = _mm_set1_epi16(255);
const __m128i rounding_add_16bit = _mm_set1_epi16(128); // For division by 255 (approx by 256)
// AVX2 每次处理 8 个像素 (32 字节)
// SSE2 每次处理 4 个像素 (16 字节)
for (; i + 3 < num_pixels; i += 4) {
// Load 4 pixels (16 bytes) of BGRA data
__m128i fg_packed_u8 = _mm_loadu_si128((const __m128i*)(fg_data + i * 4)); // B0G0R0A0 B1G1R1A1 B2G2R2A2 B3G3R3A3
__m128i bg_packed_u8 = _mm_loadu_si128((const __m128i*)(bg_data + i * 4));
// 提取前景图像的 Alpha 通道
// _mm_shuffle_epi8 可以提取间隔的数据,但这里更直接的方式是先解包
// 将 8 位 BGRA 扩展到 16 位,分为低 8 字节和高 8 字节
__m128i fg_low_u16 = _mm_unpacklo_epi8(fg_packed_u8, _mm_setzero_si128()); // B0 0 G0 0 R0 0 A0 0 B1 0 G1 0 R1 0 A1 0
__m128i fg_high_u16 = _mm_unpackhi_epi8(fg_packed_u8, _mm_setzero_si128());// B2 0 G2 0 R2 0 A2 0 B3 0 G3 0 R3 0 A3 0
__m128i bg_low_u16 = _mm_unpacklo_epi8(bg_packed_u8, _mm_setzero_si128());
__m128i bg_high_u16 = _mm_unpackhi_epi8(bg_packed_u8, _mm_setzero_si128());
// Extract Alpha values (A0, A1, A2, A3)
// Alpha is at index 6, 14 in fg_low_u16, and 6, 14 in fg_high_u16 (using epi16 indices)
__m128i alpha_low = _mm_shuffle_epi8(fg_low_u16, _mm_set_epi8(-1, -1, -1, -1, -1, -1, -1, -1, 14, 14, 6, 6, -1, -1, -1, -1)); // A1 A1 A0 A0
__m128i alpha_high = _mm_shuffle_epi8(fg_high_u16, _mm_set_epi8(-1, -1, -1, -1, -1, -1, -1, -1, 14, 14, 6, 6, -1, -1, -1, -1)); // A3 A3 A2 A2
// Repeat alpha values to match B, G, R for multiplication
// This is a common pattern for channel-wise operations.
// For each pixel: B0 G0 R0 A0. We need A0 repeated 3 times.
// We can use _mm_shuffle_epi8 for more control, or just broadcast the alpha values.
// A more robust way to get alpha for each channel:
// alpha_b_low = _mm_srli_epi16(fg_low_u16, 24) & 0xFF (This is wrong)
// We need A0, A0, A0,