arm 内联汇编基础

news2024/11/22 17:48:41

一、 Arm架构寄存器体系熟悉

基于arm neon 实现的代码有 intrinsic 和inline assembly 两种实现。

1.1 通用寄存器

arm v7 有 16 个 32-bit 通用寄存器,用 r0-r15 表示。

arm v8 有 31 个 64-bit 通用寄存器,用 x0-x30 表示,和 v7 不一样的是,这 31 个寄存器也可以作为 32-bit 寄存器来用,用 w0-w30 表示,其中 wn 是 xn 的低 32 位,如下图所示:

函数前四个参数,会按顺序被放入寄存器 r0-r3(w0-w3), 剩下会采用压栈的方式保存

寄存器寄存器别名用途
r0a1第一个函数参数: Scratch 寄存器
r1a2第二个函数参数: Scratch 寄存器
r2a3第三个函数参数: Scratch 寄存器
r3a4第四个函数参数: Scratch 寄存器
r4v1寄存器变量
r5v2寄存器变量
r6v3寄存器变量
r7v4寄存器变量
r8v5寄存器变量
r9v6 rfp寄存器变量 实际的帧指针
r10sl栈接线
r11fp参数指针
r12ip临时
r13sp栈指针
r14lr连接寄存器
r15pc程序计数

1.2 向量寄存器

armv7 包含 16 个 128-bit 向量寄存器,用 q0-q15 表示,其中每个 q 寄存器又可以拆分成两个 64-bit 向量寄存器来用,用 d0-d31 来表示。

armv8 则有更多的向量寄存器,32 个 128-bit 向量寄存器,用 v0-v31 来表示。

每个 128-bit 向量寄存器可以当做:

  • 包含 2 个 64-bit 元素的向量寄存器来用,表达形式是 vn.2d;

  • 包含 4 个 32-bit 元素的向量寄存器来用,表达形式是 vn.4s;

  • 包含 8 个 16-bit 元素的向量寄存器来用,表达形式是 vn.8h;

  • 包含 16 个 8-bit 元素的向量寄存器来用,表达形式是 vn.16b;

或者每个向量寄存器也可以只用低 64-bit:

  • 1 个 64-bit 元素的向量寄存器来用,表达形式是 vn.1d;

  • 2 个 32-bit 元素的向量寄存器来用,表达形式是 vn.2s;

  • 4 个 16-bit 元素的向量寄存器来用,表达形式是 vn.4h;

  • 8 个 8-bit 元素的向量寄存器来用,表达形式是 vn.8b;

利用指令集加速,无一例外地要利用专用寄存器这种在 CPU 上稀少、宝贵的资源。专用寄存器用少了 CPU 的性能不能充分发挥,用多了则会产生寄存器溢出 (Register Spilling)(https://blog.csdn.net/qq_41112170/article/details/90286091) 这种对性能有明显负面影响的问题。因此,我们至少需要了解在编写 Neon 代码时,有多少个专用寄存器可供利用。

二、内联汇编

2.1 基础写法

__asm__ qualifiers ( // 汇编代码部分 
: OutputOperands //在内联汇编代码中被修改的变量列表 
: InputOperands //在内联汇编代码中用到的变量列表 
: Clobbers //在内联汇编代码中用到的寄存器列表 );

qualifiers:一般是用 volatile 修饰词 ,关键字__volatile__:也可以写“volatile”,理由同上;__volatile__是可选的,作用是禁止编译器对后面汇编指令再进行优化。一般自己写的汇编,考虑性能,已经做过优化,编译器再优化的话,可能效果反而更差,所以通常还是带上这个关键字;

括号里:是真正的汇编代码,主要有四部分组成,第一部分是具体的汇编代码,是必须的;其他三个为辅助参数,可选;各部分之间用冒号“:”分割,即使参数为空,也要加冒号;

  • OutputOperands:在内联汇编中会被修改的变量列表,变量之间用','隔开, 每个变量的格式是: [asmSymbolicName] "constraint"(cvariablename) cvariablename:表示变量原来的名字; asmSymbolicName:表示变量在内联汇编代码中的别名,一般和 cvariablename 一样,在汇编代码中就可以通过%[asmSymbolicName]去使用该变量; constraint: 一般填=r,具体解释见文档[6]

  • InputOperands:在内联汇编中用到的所有变量列表,变量之间用','隔开, 每个变量的格式是: [asmSymbolicName] "constraint"(cexpression) 和输出不一样地方是,首先要按OutputOperands列表的顺序再列一遍,但是constraint用数字代替从0开始,然后才是写其他只读变量,只读变量constraintr

  • Clobbers: 一般是"cc", "memory"开头,然后接着填内联汇编中用到的通用寄存器和向量寄存器 "cc"表示内联汇编代码修改了标志寄存器; "memory"表示汇编代码对输入和输出操作数执行内存读取或写入操作(读写参数列表之一的变量指向的内存);

  • 输入列表 ("r" (some_input)): 这表明 some_input 是一个输入操作数,它的值在汇编执行前被读取。"r" 约束表示 some_input 被存储在某个寄存器中,具体哪个寄存器由编译器决定。

  • 输出列表 ("+r" (result)): 这表明 result 是一个输出操作数,它的值在汇编执行后被写回。"+" 约束表示 result 既可以作为输入也可以作为输出,汇编代码可以读取它的初始值,并在执行过程中更新它的值。

约束说明:

  • "r":将值放入任意一个可用的寄存器中。

  • "+r":将值放入任意一个可用的寄存器中,并且该寄存器在操作后还会被写回,即它既可以作为输入也可以作为输出。

  • "+w":类似于 "+r",但表示该值在汇编代码中可能会被修改,并且修改后的值需要写回原始变量。

  • "m":表示该值应该被加载到内存地址中,通常与指针一起使用。

asm("mov %0,%1"
        :"+r"(val1)
        :"r"(val2)
        :);

由上面对指令语法的描述进行分析:

  • 输出操作数为 val1,属性为 "=r"。

  • 输入操作数为 val2,属性为 "r"

  • code 部分为 mov %1,%0,

  • %0 表示输入输出列表中的第一个操作数,

  • %1 表示操作数列表中提供的第二个操作数,以此类推,这条汇编指令很明显就是将第二个操作数(val2)赋值给第一个操作数(val1),所以最后的结果为 val1 = 222. 。

    int x=10, y;
    __asm__ ("mov %[in],%[out]"
       : [out]"=r"(y)
       : [in]"r"(x)
       :
    );
    

如果指定了别名的话,那在汇编模板中,引用该变量,就可以使用别名,增加可读性,

2.2 操作符含义

  • "=" 表示只写,通常用于所有输出操作数的属性

  • "+" 表示读写,只能被列为输出操作数的属性,否则编译会报错。

  • & :只能用作输出

限定符

ARM指令集含义

r

通用寄存器

f

浮点寄存器

m

内存地址

为保持寄存器,内存数据一致性,提供三个类型

类型作用
r0…r15告诉编译器汇编代码中修改了寄存器r0…r15 (v8 是x, v)
cc告诉编译器汇编代码会导致CPU状态位的改变
memory告诉编译器汇编代码会读取或修改内存中某个地址存放的值

三、样例分析

对于刚入门优化的同学,改写汇编最好先从 C++ 改写 intrinsic 开始,然后再根据 intrinsic 的代码去改写汇编,一般 intrinsic 的指令和汇编指令都能对应的上,当然高手可以直接跳过去写汇编,但是对于新手建议还是一步步来。

而且比较重要的一点是,我认为 算法上的改进更为重要,假设你 C++ 算法层面代码已经定下来了,对于性能还想有更进一步的提升,那么可以尝试去写 neon 汇编(内联或者纯汇编),但不是说汇编是万能的,这个和你的优化经验还有算法本身的复杂度有很大关系,可能你吭哧坑次改完,发现还做了负优化,因为编译器本身也会做向量化。

3.1 两个数组加权和

第一个例子是两个数组对应元素加权和,例子足够简单,方便讲解改写汇编的一些思路。 下面代码为了可读性会相应的作简.

3.1.1 c++ 实现

bool arrWeightedAvg(const float *arr1,
                    const float arr1Weight,
                    const float *arr2,
                    const float arr2Weight,
                    const int len,
                    float *resultArr) {
  for (int i = 0; i < len; ++i) {
    resultArr[i] = arr1[i] * arr1Weight + arr2[i] * arr2Weight;
  }
  return true;
}

3.1.2 改 intrinsic

对于 intrinsic 代码是兼容 armv7 和 v8 的,所以不同架构之间迁移也方便,不需要改代码:

bool arrWeightedAvgIntrinsic(const float *arr1,
                             const float arr1Weight,
                             const float *arr2,
                             const float arr2Weight,
                             const int len,
                             float *resultArr) {
  int neonLen = len >> 2;
  int remain = len - (neonLen << 2);
  // 这里向量化主要思路是循环内每次
  // 处理4个元素的加权和
  // 所以neonLen是数组长度len除4
  // 而剩下的尾部元素按正常处理

  float *resultArrPtr  = resultArr;
  const float *arr1Ptr = arr1;
  const float *arr2Ptr = arr2;

  // 因为一次处理4个元素
  // 所以权值要拷贝4份放到
  // 一个float32x4_t类型变量中
  // 也相当于是128-bit向量寄存器
  float32x4_t arr1Wf4 = vdupq_n_f32(arr1Weight);
  float32x4_t arr2Wf4 = vdupq_n_f32(arr2Weight);

  for (int i = 0; i < neonLen; ++i) {
    // 分别读4个数组元素
    float32x4_t arr1f4 = vld1q_f32(arr1Ptr);
    float32x4_t arr2f4 = vld1q_f32(arr2Ptr);

    // eltwise乘法
    arr1f4 = vmulq_f32(arr1f4, arr1Wf4);
    arr2f4 = vmulq_f32(arr2f4, arr2Wf4);

    // eltwise加法
    float32x4_t resultf4 = vaddq_f32(arr1f4, arr2f4);

    // 写结果
    vst1q_f32(resultArrPtr, resultf4);

    arr1Ptr += 4;
    arr2Ptr += 4;
    resultArrPtr += 4;
  }
  // 处理尾部元素
  for (; remain > 0; remain --) {
    *resultArrPtr = (*arr1Ptr) * arr1Weight + (*arr2Ptr) * arr2Weight;
    resultArrPtr ++;
    arr1Ptr ++;
    arr2Ptr ++;
  }

  return true;
}

3.1.3 arm v7 内联汇编

bool arrWeightedAvgIntrinsic(const float *arr1,
                             const float arr1Weight,
                             const float *arr2,
                             const float arr2Weight,
                             const int len,
                             float *resultArr) {
  int neonLen = len >> 2;
  int remain = len - (neonLen << 2);
  // 这里向量化主要思路是循环内每次
  // 处理4个元素的加权和
  // 所以neonLen是数组长度len除4
  // 而剩下的尾部元素按正常处理

  float *resultArrPtr  = resultArr;
  const float *arr1Ptr = arr1;
  const float *arr2Ptr = arr2;

  // 因为一次处理4个元素
  // 所以权值要拷贝4份放到
  // 一个float32x4_t类型变量中
  // 也相当于是128-bit向量寄存器
  float32x4_t arr1Wf4 = vdupq_n_f32(arr1Weight);
  float32x4_t arr2Wf4 = vdupq_n_f32(arr2Weight);

  for (int i = 0; i < neonLen; ++i) {
    // 分别读4个数组元素
    float32x4_t arr1f4 = vld1q_f32(arr1Ptr);
    float32x4_t arr2f4 = vld1q_f32(arr2Ptr);

    // eltwise乘法
    arr1f4 = vmulq_f32(arr1f4, arr1Wf4);
    arr2f4 = vmulq_f32(arr2f4, arr2Wf4);

    // eltwise加法
    float32x4_t resultf4 = vaddq_f32(arr1f4, arr2f4);

    // 写结果
    vst1q_f32(resultArrPtr, resultf4);

    arr1Ptr += 4;
    arr2Ptr += 4;
    resultArrPtr += 4;
  }
  // 处理尾部元素
  for (; remain > 0; remain --) {
    *resultArrPtr = (*arr1Ptr) * arr1Weight + (*arr2Ptr) * arr2Weight;
    resultArrPtr ++;
    arr1Ptr ++;
    arr2Ptr ++;
  }

  return true;
}

3.1.4 armv8 内联汇编

#ifdef __aarch64__  // armv8
  __asm__ volatile(
    "mov   x0, %[arr1Weight]                  \n"   // 将weight1的值移动到通用寄存器x0中。
    "dup   v0.4s, w0                          \n"   //w0是x0的低32位, 复制值到向量寄存器v0中,当成4*32来使用。
    
    "mov   x1, %[arr2Weight]                  \n"
    "dup   v1.4s, w1                          \n"
    
    "0:                                       \n"    //循环结束条件,小于0.

    "prfm  pldl1keep, [%[arr1Ptr], #128]      \n"    //预读取arr1地址开始的128bit 数据,就是4个32bit的数据。
    "ld1      {v2.4s}, [%[arr1Ptr]], #16      \n"    // 将数据加载到v2 向量寄存器中, 并且地址自增16个字节。

    "prfm  pldl1keep, [%[arr2Ptr], #128]      \n"
    "ld1      {v3.4s}, [%[arr2Ptr]], #16      \n"
    
    "fmul       v4.4s, v2.4s, v0.4s           \n"    //数组1和权重相乘。保存在v4 寄存器中。
    "fmul       v5.4s, v3.4s, v1.4s           \n"    // 数据2和权重相乘,保存在v5 寄存器中。

    "fadd       v6.4s, v4.4s, v5.4s           \n"   //将寄存器v4, v5的值相加, 保存在v6寄存器中。

    "subs       %[neonLen], %[neonLen], #1    \n"  // 对应 neonLen--  sub指令后面加个s表示会更新条件flag
   
    
    "st1    {v6.4s}, [%[resultArrPtr]], #16   \n" //将寄存器v6的结果写入到目的地址resultarrptr, 地址自增16字节。(4个数,一个数四字节)
    
    "bgt        0b                            \n"  //b跳转指令, gt 判断是不是大于0条件判断, 大于0, 跳转到0的位置。

    :[arr1Ptr]        "+r"(arr1Ptr),
     [arr2Ptr]        "+r"(arr2Ptr),
     [resultArrPtr]   "+r"(resultArrPtr),
     [neonLen]        "+r"(neonLen)
    :[arr1Weight]     "r"(arr1Weight),
     [arr2Weight]     "r"(arr2Weight)
    :"cc", "memory", "x0", "x1", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7"
  );

反编译系统的编译文件,进行汇编代码对比,学习。

./llvm-objdump -d /home/lsq/wind/wind_develop_my/wind/build_android/src/backend/cpu/CMakeFiles/Wi

ndCPU.dir/kernel/neon/matmul_quant_test.cc.o

内联汇编的目的是进行汇编指令的优化,尽可能的直接操作寄存器,利用新特性,进行代码的加速。更多的指令需要查找官方文档进行学习。

3.2 汇编指令对应的机器码生成

.inst 0x4e80a4d8 是一个汇编指令,用于在 ARM 架构中直接插入机器码。这个指令的格式是 .inst <机器码>,其中 <机器码> 是一个 32 位或 64 位的十六进制值,表示一条机器指令。

具体来说,0x4e80a4d8 是一个 32 位的机器码。为了理解这个机器码是如何编码的,我们需要查看 ARMv8 指令集的文档,特别是 NEON 指令集的文档。

3.2.1 使用 LLVM 工具

可以安装 llvm 工具链,然后运行如下命令:、

echo "smmla v16.4s, v4.16b, v0.16b" | llvm-mc -arch=aarch64 -mattr=+neon,+i8mm -show-encoding

这将会输出汇编指令对应的机器码。如果没有安装 llvm-mc 工具,可以参考以下汇编器指令来生成机器码。

3.2.2 使用 GNU 汇编器

你可以使用 arm-none-eabi-as 工具来编译汇编代码并生成机器码。下面是一个示例:

echo ".arch armv8-a; smmla v16.4s, v4.16b, v0.16b" | arm-none-eabi-as -o - -a -

3.2.3 在线工具

https://armconverter.com/

3.2.4 反编译编译产物

./llvm-objdump -d /home/lsq/wind/wind_develop_my/wind/build_android/src/backend/cpu/CMakeFiles/WindCPU.dir/kernel/neon/matmul_quant_test.cc.o 反汇编结果:

".inst 0x4e80a490 \n" // smmla v16.4s, v4.16b, v0.16b //v0_01s

// *y0_0

".inst 0x4e81a4b5 \n" // smmla v21.4s, v5.16b, v1.16b //v0_0hs

// *y0_1

".inst 0x4e82a4da \n" // smmla v26.4s, v6.16b, v2.16b //v0_1ls

// *y0_2

".inst 0x4e83a4ff \n" // smmla v31.4s, v7.16b, v3.16b// v0_1hs

四、附录

https://medium.com/@warmap_/%E8%BD%AC-%E5%A6%82%E4%BD%95%E5%9C%A8c%E6%88%96c-%E4%BB%A3%E7%A0%81%E4%B8%AD%E5%B5%8C%E5%85%A5arm%E6%B1%87%E7%BC%96%E4%BB%A3%E7%A0%81-a3704e164de8

http://giantpandacv.com/project/%E9%83%A8%E7%BD%B2%E4%BC%98%E5%8C%96/AI%20%E7%A7%BB%E5%8A%A8%E7%AB%AF%E7%AE%97%E6%B3%95%E4%BC%98%E5%8C%96/%E7%A7%BB%E5%8A%A8%E7%AB%AFarm%20cpu%E4%BC%98%E5%8C%96%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E7%A7%BB%E5%8A%A8%E7%AB%AFarm%20cpu%E4%BC%98%E5%8C%96%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E7%AC%AC4%E5%BC%B9--%E5%86%85%E8%81%94%E6%B1%87%E7%BC%96%E5%85%A5%E9%97%A8/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1936991.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AndroidStudio与手机进行无线调试

(一)、前提条件 一部手机一条USB数据线一部电脑手机和电脑连接到同一个 Wifi开启手机的USB调试功能开启手机的无线调试功能 (二)、操作步骤 1、 将手机和电脑用USB数据线连接 2、 打开 终端&#xff0c;输入 adb devices ,查看手机和电脑是否连接成功。如下图&#xff1a; 2、…

Mac电脑硬盘无法读取怎么办 Mac硬盘读取慢会导致电脑卡吗 Paragon NTFS 激活码

Mac电脑作为众多用户的工作与生活伙伴&#xff0c;其稳定性和效率至关重要。然而&#xff0c;当遭遇硬盘无法读取或读取速度缓慢的问题时&#xff0c;这无疑会给用户带来极大的不便&#xff0c;影响日常操作与数据访问。下面我们来看看Mac电脑硬盘无法读取怎么办&#xff0c;Ma…

Kotlin性能:runCatching、try-catch块、无异常处理耗时比较

Kotlin性能&#xff1a;runCatching、try-catch块、无异常处理耗时比较 fun main(args: Array<String>) {val a 1.1fval b 2.2fval n1 System.nanoTime()val m1 System.currentTimeMillis()runCatching {cal(a, b)}val n2 System.nanoTime()val m2 System.currentT…

python中format函数和f-string详解

Python 中的 format() 函数是一种字符串格式化的方法&#xff0c;通过花括号 {} 来定义占位符&#xff0c;并通过 format() 方法中的参数来替换这些占位符。这种方法非常灵活&#xff0c;可以处理多种数据类型&#xff0c;并且可以通过索引、关键字或者属性名来访问对象的属性。…

【中项】系统集成项目管理工程师-第2章 信息技术发展-2.2新一代信息技术及应用-2.2.3大数据与2.2.4区块链

前言&#xff1a;系统集成项目管理工程师专业&#xff0c;现分享一些教材知识点。觉得文章还不错的喜欢点赞收藏的同时帮忙点点关注。 软考同样是国家人社部和工信部组织的国家级考试&#xff0c;全称为“全国计算机与软件专业技术资格&#xff08;水平&#xff09;考试”&…

实战项目:仿muduo库实现并发服务器

目录 项目初始与项目演示HTTP服务器基础认识Reactor模式基础认识单Reactor单线程模式认识单Reactor多线程模式认识多Reactor多线程模式认识 目标定位总体大模块划分server模块的管理思想Buffer子模块Socket子模块Channel子模块Connection子模块Acceptor子模块TimerQueue子模块P…

记录些Spring+题集(9)

在网络应用开发中&#xff0c;Cookie、Session 和 Token 是常见的用于管理用户状态和身份验证的机制。 Cookie、Session、Token 的区别 这三个概念的产生与 HTTP 是一种无状态协议密切相关。在 HTTP 中&#xff0c;每次客户端与服务器之间进行通信时&#xff0c;服务器无法直…

8月长沙学术会议:经济金融发展国际会议(ICEFD 2024)

经济金融发展国际会议&#xff08;ICEFD 2024&#xff09;将于2024年8月20-22日在长沙盛大举行。本次会议汇聚了国内外众多高校、科研机构、企业界及其他相关领域的专家学者&#xff0c;共同探讨经济金融发展的新趋势、新挑战和新机遇。 长沙&#xff0c;这座历史悠久的文化名…

细说MCU用DMA控制ADC采样和串口传送的实现方法

目录 一、建立工程 1.相同的配置 2.配置ADC 3.配置DMA 二、代码修改 1.定义存储ADC采样结果的数组 2.启动ADC与定时器 3.编写主程序代码 4.重定义回调函数 5.查看结果 三、修改DMA模式 1. 修改DMA模式为Circular 2.查看结果 采用DMA(Direct Memory Access&#xf…

Python爬虫(1) --基础知识

爬虫 爬虫是什么&#xff1f; spider 是一种模仿浏览器上网过程的一种程序&#xff0c;可以获取一些网页的数据 基础知识 URL 统一资源定位符 uniform resource locator http: 超文本传输协议 HyperText Transfer Protocol 默认端口 80 https: 安全的超文本传输协议 security…

docker 部署wechatbot-webhook 并获取接口实现微信群图片自动保存到chevereto图库等

功能如图&#xff1a; docker部署 version: "3" services:excalidraw:image: dannicool/docker-wechatbot-webhook:latestcontainer_name: wechatbot-webhookdeploy:resources:limits:cpus: 0.15memory: 500Mreservations:cpus: 0.05memory: 80Mrestart: alwayspor…

数据结构_Map和Set

目录 一、搜索模型 二、Map 2.1 Map.Entry 2.2 Map 方法 2.3 Map 注意事项 三、Set 3.1 Set 方法 3.2 Set 注意事项 四、哈希表 4.1 哈希表 4.2 冲突 4.3 哈希函数设计 4.4 闭散列 4.5 开散列/哈希桶 总结 【搜索树】 二叉搜索树又称二叉排序树&#xff0c;它或…

P4-AI产品经理-九五小庞

从0开始做AI产品的完整工作方法 项目启动 项目实施 样本测试模型推荐引擎 构建DMP&#xff08;数据管理平台&#xff09; 项目上线

Intel和AMD用户再等等!微软确认Win11 24H2年底前登陆

微软近日确认&#xff0c;Windows 11 24H2版本将于2024年底前正式登陆使用英特尔和AMD处理器的PC。 根据微软介绍&#xff0c;Windows 11 24H2将作为传统功能更新&#xff0c;将在今年晚些时候提供给所有设备。 此前&#xff0c;微软已向搭载骁龙X Plus和X Elite系列处理器的Co…

2024年计算机软考中级【硬件工程师】面试题目汇总(附答案)

硬件工程师面试题汇总分析 1、解释一下同步电路和异步电路 解题思路 同步电路和异步电路是指同步时序电路和异步时序电路。由于存储电路中触发器的动作特点不同&#xff0c;因此可以把时序电路分为同步时序电路和异步时序电路两种。同步时序电路所有的触发器状态的变化都是在同…

pytorch学习(七)torchvision.datasets的使用

网络上已经有公开的数据集&#xff0c;并且这些数据集被整合到了torchvision.datasets中&#xff0c;使用自带的函数可以直接下载。 1.数据集 具体有哪些数据可直接用torchvision.datasets加载呢&#xff1f;可以查看这个网址&#xff1a; datasets官网&#xff1a;Datasets…

Windows11+VS2019+Trimesh2 2.16 安装配置记录

Windows11VS2019Trimesh2 2.16 编译配置记录 trimesh2下载在VS进行配置trimesh2 vs2019编译 若无特别说明均在Release x64平台下配置测试 trimesh2下载 TriMesh2官网 的自述文件里说其新版本不能直接使用MSVC编译&#xff0c;但在Github找到一个大佬的仓库&#xff0c;该大佬直…

多口适配器,给您的生活增添便利

随着科技的快速发展&#xff0c;我们的生活已离不开各种各样的电子设备&#xff0c;智能手机、平板电脑、智能手表、无线耳机……它们共同构建了我们丰富多彩的数字生活。然而&#xff0c;面对众多设备的充电需求&#xff0c;传统的单一充电口已难以满足现代人的使用习惯。在这…

安卓系统签名的制作与使用(SignApk.jar)踩坑记录

看到这里的你应该能区分apk签名跟系统签名吧,如果无法区分的请看下面链接 android 应用的证书签名跟系统签名 看过上面的文章应该知道系统签名需要的文件清单大概有哪些 前两个是编译安卓系统时在build目录下,详细目录为 /build/target/product/security 每组签名用途不同&am…

IP转接服务的重要性及其应用

在现今互联网高度发达的时代&#xff0c;IP转接服务的重要性日益凸显。对于家庭和企业而言&#xff0c;它不仅是连接互联网的桥梁&#xff0c;更是确保网络稳定、高效运行的关键。本文将深入探讨IP转接服务的核心意义及其在互联网世界中的应用。 IP转接服务&#xff0c;简而言之…