【CUDA】 矩阵乘法 matMatMul

矩阵乘法 matMatMul

矩阵乘法是基本线性代数子程序(BLAS)的重要组成部分,而且线性代数中许多其他操作以此为基础。

图1是两个矩阵的乘法。

基础方法,正方形tile和长方形tile

基础方法

执行矩阵乘法的基础方法是使用单个线程执行输出矩阵的单个元素乘法。这意味着所需的线程数等于输出矩阵中的元素数。线程排列在二维网格中,每个线程分配一个唯一的索引。线程的索引用于访问输入矩阵的相应行和列。执行行和列的乘法,结果存储在输出矩阵的相应元素中。

这种方法的主要缺点是多个线程加载相同的行和列来计算它们的输出。图2是一个示例。

图2

正如上图所示,为了计算P0,0和P0,1,两个线程都需要加载整个M0。对于P0,0和P1,0也是如此。两个线程都需要加载整个N1列。这意味着线程将多次访问相同的内存位置。

正方形tile

为了避免这个问题,我们可以使用tile。tile是将输入矩阵的一个小部分加载到共享内存中。线程将把tile加载到共享内存中,然后执行乘法运算。图3描述了这种技术。

图3

kernel将计算分为几个阶段。每个阶段,线程将将M和N矩阵中的tile加载到共享内存中。然后,线程将执行tile的乘法,并将结果累积到输出矩阵的相应元素中。

通过这种技术,我们可以看到全局内存访问减少了TILE_WIDTH(图示)倍。

长方形tile

为了进一步减少全局内存访问,我们可以使用图4所示的矩形块。

图4

通过这种技术,我们增加了每个线程的工作量,以进一步减少全局内存访问的次数。kernel再次将计算分成多个阶段。在每个阶段中,线程将从中加载一个tile M 和两个tile N 到共享内存中。然后线程将对这些tile进行乘法运算,并将结果累加到输出矩阵的相应元素中。


Code

host代码使用随机值初始化输入矩阵,并调用kernel执行乘法运算。输入矩阵以线性格式存储。


#include <iostream>#include <thrust/host_vector.h>
#include <thrust/device_vector.h>
#include <thrust/transform.h>#include "mat_mat_mul_kernels.cuh"int main(int argc, char *argv[])
{int rows1, cols1rows2, cols2, t_x;if(argc != 5){std::cout << "Usage: " << argv[0] << " <rows1> <cols1rows2> <cols2> <t_x>" << std::endl;return 1;}rows1 = atoi(argv[1]);cols1rows2 = atoi(argv[2]);cols2 = atoi(argv[3]);t_x = atoi(argv[4]);thrust::host_vector<float> h_in_mat1(rows1 * cols1rows2);thrust::host_vector<float> h_in_mat2(cols1rows2 * cols2);thrust::host_vector<float> h_out_host(rows1 * cols2);srand(time(NULL));for(int i = 0; i < rows1 * cols1rows2; i++)h_in_mat1[i] = rand() / (float)RAND_MAX;for(int i = 0; i < cols1rows2 * cols2; i++)h_in_mat2[i] = rand() / (float)RAND_MAX;for(int i = 0; i < rows1; ++i)for(int j = 0; j < cols2; ++j)for(int k = 0; k < cols1rows2; ++k)h_out_host[i * cols2 + j] += h_in_mat1[i * cols1rows2 + k] * h_in_mat2[k * cols2 + j];thrust::device_vector<float> d_in_mat1 = h_in_mat1;thrust::device_vector<float> d_in_mat2 = h_in_mat2;thrust::device_vector<float> d_out(rows1 * cols2);dim3 blockDim(t_x, t_x);dim3 gridDim((cols2 + blockDim.x - 1) / blockDim.x, (rows1 + blockDim.y - 1) / blockDim.y);mat_mat_mul<float><<<gridDim, blockDim>>>(thrust::raw_pointer_cast(d_in_mat1.data()),thrust::raw_pointer_cast(d_in_mat2.data()),thrust::raw_pointer_cast(d_out.data()),rows1, cols1rows2, cols1rows2, cols2);bool success = true;for(int i = 0; i < rows1 * cols2; ++i)if(abs(h_out_host[i] - d_out[i]) >= 0.001){std::cout << "Error at " << i << ": " << h_out_host[i] << " != " << d_out[i] << " (Mat Mat Mul)" << std::endl;success = false;break;}if(success)std::cout << "Success (Mat Mat Mul)" << std::endl;blockDim = dim3(t_x, t_x);gridDim = dim3((cols2 + blockDim.x - 1) / blockDim.x, (rows1 + blockDim.y - 1) / blockDim.y);mat_mat_mul_tiles<float><<<gridDim, blockDim, 2 * blockDim.x * blockDim.x * sizeof(float)>>>(thrust::raw_pointer_cast(d_in_mat1.data()),thrust::raw_pointer_cast(d_in_mat2.data()),thrust::raw_pointer_cast(d_out.data()),rows1, cols1rows2, cols1rows2, cols2);success = true;for(int i = 0; i < rows1 * cols2; ++i)if(abs(h_out_host[i] - d_out[i]) >= 0.001){std::cout << "Error at " << i << ": " << h_out_host[i] << " != " << d_out[i] << " (Mat Mat Mul Tiles)" << std::endl;success = false;break;}if(success)std::cout << "Success (Mat Mat Mul Tiles)" << std::endl;blockDim = dim3(t_x, t_x);gridDim = dim3((cols2 + blockDim.x - 1) / blockDim.x / 2, (rows1 + blockDim.y - 1) / blockDim.y);mat_mat_mul_rec_tiles<float><<<gridDim, blockDim, 3 * blockDim.x * blockDim.x * sizeof(float)>>>(thrust::raw_pointer_cast(d_in_mat1.data()),thrust::raw_pointer_cast(d_in_mat2.data()),thrust::raw_pointer_cast(d_out.data()),rows1, cols1rows2, cols1rows2, cols2);success = true;for(int i = 0; i < rows1 * cols2; ++i)if(abs(h_out_host[i] - d_out[i]) >= 0.001){std::cout << "Error at " << i << ": " << h_out_host[i] << " != " << d_out[i] << " (Mat Mat Mul Rec Tiles)" << std::endl;success = false;break;}if(success)std::cout << "Success (Mat Mat Mul Rec Tiles)" << std::endl;return 0;}

以下是kernel的展示。

基础方法

template<typename T> __global__
void mat_mat_mul(T *in_mat1, T *in_mat2, T *out_mat, int mat1_rows, int mat1_cols, int mat2_rows, int mat2_cols) {int row = blockIdx.y * blockDim.y + threadIdx.y;int col = blockIdx.x * blockDim.x + threadIdx.x;if (row >= mat1_rows || col >= mat2_cols) return;T sum = 0;for (int k = 0; k < mat1_cols; ++k)sum += in_mat1[row * mat1_cols + k] * in_mat2[k * mat2_cols + col];out_mat[row * mat2_cols + col] = sum;
}

在这种基本方法中,kernel非常简单。

线程首先计算它们的行和列索引。如果行或列索引大于输入矩阵的行数或列数,则线程返回。

int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;if (row >= mat1_rows || col >= mat2_cols) return;

然后,这些线程会对行和列进行相乘,并将结果存储在输出矩阵的相应元素中。

T sum = 0;
for (int k = 0; k < mat1_cols; ++k)sum += in_mat1[row * mat1_cols + k] * in_mat2[k * mat2_cols + col];out_mat[row * mat2_cols + col] = sum;

我们可以观察到,线程不仅会多次加载相同的行和列,而且它们还会以非合并的方式加载M的元素。这意味着线程将无法充分利用内存带宽。


正方形tile

template<typename T> __global__
void mat_mat_mul_tiles(T *in_mat1, T *in_mat2, T *out_mat,int mat1_rows, int mat1_cols,int mat2_rows, int mat2_cols) {// Initialize shared memoryint TILE_WIDTH = blockDim.x;extern __shared__ uint8_t shared_mem[];T *ds_mat1 = reinterpret_cast<T*>(shared_mem);T *ds_mat2 = reinterpret_cast<T*>(shared_mem + TILE_WIDTH * TILE_WIDTH * sizeof(T));int bx = blockIdx.x,  by = blockIdx.y;int tx = threadIdx.x, ty = threadIdx.y;int row = by * TILE_WIDTH + ty;int col = bx * TILE_WIDTH + tx;T out_value = 0;// Loop over the in_mat1 and in_mat2 tiles required to compute the out_mat elementfor (int ph = 0; ph < (mat1_cols + TILE_WIDTH - 1) / TILE_WIDTH; ++ph) {// Collaborative loading of in_mat1 and in_mat2 tiles into shared memoryds_mat1[ty * TILE_WIDTH + tx] = row < mat1_rows && ph * TILE_WIDTH + tx < mat1_cols ?in_mat1[row * mat1_cols + ph * TILE_WIDTH + tx] : 0;ds_mat2[ty * TILE_WIDTH + tx] = ph * TILE_WIDTH + ty < mat2_rows && col < mat2_cols ?in_mat2[(ph * TILE_WIDTH + ty) * mat2_cols + col] : 0;// Synchronize to make sure the tiles are loaded__syncthreads();// Compute the out_mat elementfor (int k = 0; k < TILE_WIDTH; ++k)out_value += ds_mat1[ty * TILE_WIDTH + k] * ds_mat2[k * TILE_WIDTH + tx];// Synchronize to make sure the out_mat element is computed// before other threads load new tiles__syncthreads();}// Store the out_mat element in out_matif (row < mat1_rows && col < mat2_cols)out_mat[row * mat2_cols + col] = out_value;
}

在这种方法中,我们使用共享内存来存储输入矩阵的块。线程首先计算它们的行和列索引。如果行或列索引大于输入矩阵的行数或列数,则线程返回。

int row = by * TILE_WIDTH + ty;
int col = bx * TILE_WIDTH + tx;if (row >= mat1_rows || col >= mat2_cols) return;

在每个阶段:

线程将tiles加载到共享内存中。 这些tiles以合并访存的方式加载。

// Collaborative loading of in_mat1 and in_mat2 tiles into shared memory
ds_mat1[ty * TILE_WIDTH + tx] = row < mat1_rows && ph * TILE_WIDTH + tx < mat1_cols ?in_mat1[row * mat1_cols + ph * TILE_WIDTH + tx] : 0;ds_mat2[ty * TILE_WIDTH + tx] = ph * TILE_WIDTH + ty < mat2_rows && col < mat2_cols ?in_mat2[(ph * TILE_WIDTH + ty) * mat2_cols + col] : 0;// Synchronize to make sure the tiles are loaded
__syncthreads();

当线程加载完tile后,它们会计算输出矩阵元素。线程通过将tiles的相应行和列相乘,并将结果相加来计算输出矩阵元素。

// Compute the out_mat element
for (int k = 0; k < TILE_WIDTH; ++k)out_value += ds_mat1[ty * TILE_WIDTH + k] * ds_mat2[k * TILE_WIDTH + tx];// Synchronize to make sure the out_mat element is computed
// before other threads load new tiles
__syncthreads();

最后,线程将输出矩阵元素存储在输出矩阵中。

// Store the out_mat element in out_mat
out_mat[row * mat2_cols + col] = out_value;

长方形tiles

template<typename T> __global__
void mat_mat_mul_rec_tiles(T* in_mat1, T* in_mat2, T* out_mat,int mat1_rows, int mat1_cols,int mat2_rows, int mat2_cols) {// Initialize shared memoryint TILE_WIDTH = blockDim.x;extern __shared__ uint8_t shared_mem[];T* ds_mat1 = reinterpret_cast<T*>(shared_mem);T* ds_mat2 = reinterpret_cast<T*>(shared_mem + TILE_WIDTH * TILE_WIDTH * sizeof(T));T* ds_mat3 = reinterpret_cast<T*>(shared_mem + 2 * TILE_WIDTH * TILE_WIDTH * sizeof(T));int bx = blockIdx.x, by = blockIdx.y;int tx = threadIdx.x, ty = threadIdx.y;int row = by * TILE_WIDTH + ty;int col = bx * TILE_WIDTH * 2 + tx;T out_value1 = 0;T out_value2 = 0;// Loop over the in_mat1 and in_mat2 tiles required to compute the out_mat elementfor (int ph = 0; ph < (mat1_cols + TILE_WIDTH - 1) / TILE_WIDTH; ++ph) {// Collaborative loading of in_mat1 and in_mat2 tiles into shared memoryds_mat1[ty * TILE_WIDTH + tx] = row < mat1_rows&& ph* TILE_WIDTH + tx < mat1_cols ?in_mat1[row * mat1_cols + ph * TILE_WIDTH + tx] : 0;ds_mat2[ty * TILE_WIDTH + tx] = ph * TILE_WIDTH + ty < mat2_rows&& col < mat2_cols ?in_mat2[(ph * TILE_WIDTH + ty) * mat2_cols + col] : 0;ds_mat3[ty * TILE_WIDTH + TILE_WIDTH + tx] = ph * TILE_WIDTH + ty < mat2_rows&& TILE_WIDTH + col < mat2_cols ?in_mat2[(ph * TILE_WIDTH + ty) * mat2_cols + TILE_WIDTH + col] : 0;// Synchronize to make sure the tiles are loaded__syncthreads();// Compute the out_mat elementfor (int k = 0; k < TILE_WIDTH; k++) {out_value1 += ds_mat1[ty * TILE_WIDTH + k] * ds_mat2[k * TILE_WIDTH + tx];out_value2 += ds_mat1[ty * TILE_WIDTH + k] * ds_mat3[k * TILE_WIDTH + TILE_WIDTH + tx];}// Synchronize to make sure the Pvalues are computed// before other threads load new tiles__syncthreads();}// Store the Pvalues in out_matif (row < mat1_rows && col < mat2_cols)//out_mat[row][col];out_mat[row * mat2_cols + col] = out_value1;if (row < mat1_rows && TILE_WIDTH + col < mat2_cols)//out_mat[row][TILE_WIDTH + col];out_mat[row * mat2_cols + TILE_WIDTH + col] = out_value2;
}

这个kernel函数几乎与正方形tile函数相同。

首先,线程计算出他们将计算的输出矩阵元素的行和列。

int row = by * TILE_WIDTH     + ty;
int col = bx * TILE_WIDTH * 2 + tx;

然后线程将tile加载到共享内存中。唯一的区别是N加载2个tile。

// Collaborative loading of in_mat1 and in_mat2 tiles into shared memoryds_mat1[ty * TILE_WIDTH + tx] = row < mat1_rows&& ph* TILE_WIDTH + tx < mat1_cols ?in_mat1[row * mat1_cols + ph * TILE_WIDTH + tx] : 0;ds_mat2[ty * TILE_WIDTH + tx] = ph * TILE_WIDTH + ty < mat2_rows&& col < mat2_cols ?in_mat2[(ph * TILE_WIDTH + ty) * mat2_cols + col] : 0;ds_mat3[ty * TILE_WIDTH + TILE_WIDTH + tx] = ph * TILE_WIDTH + ty < mat2_rows&& TILE_WIDTH + col < mat2_cols ?in_mat2[(ph * TILE_WIDTH + ty) * mat2_cols + TILE_WIDTH + col] : 0;

然后线程计算两个输出矩阵元素。

// Compute the out_mat elementfor (int k = 0; k < TILE_WIDTH; k++) {out_value1 += ds_mat1[ty * TILE_WIDTH + k] * ds_mat2[k * TILE_WIDTH + tx];out_value2 += ds_mat1[ty * TILE_WIDTH + k] * ds_mat3[k * TILE_WIDTH + TILE_WIDTH + tx];}

最后,存储两个输出矩阵元素。

// Store the Pvalues in out_mat
if (row < mat1_rows && col < mat2_cols)//out_mat[row][col];out_mat[row * mat2_cols + col] = out_value1;if (row < mat1_rows && TILE_WIDTH + col < mat2_cols)//out_mat[row][TILE_WIDTH + col];out_mat[row * mat2_cols + TILE_WIDTH + col] = out_value2;

性能分析

运行时间:

矩阵维度:1024 × 1024

线程块维度:32 × 32

可见使用共享内存可以有效降低运行速度。但长方形tile反而耗时更久。因为L1缓存与共享内存公用硬件空间。可能共享内存占据大部分空间,而L1缓存所剩无几,从而导致长方形tile耗时更久。具体情况需要做性能分析,之后再补充。也许是长方形tile的复杂性增加。kernel引入了许多分支和同步点。这可能导致耗时更久。

笔者采用设备:RTX3060 6GB

PMPP项目提供的分析

kernel的性能是使用NvBench项目在多个gpu中测量的。研究的性能测量方法有:

内存带宽:每秒传输的数据量。

内存带宽利用率:占用内存带宽的百分比。

基础方法

正方形tile

长方形tile

参考文献:

1、大规模并行处理器编程实战(第2版)

2、PPMP

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

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

相关文章

传统视觉Transformer的替代者:交叉注意力Transformer(CAT)

传统视觉Transformer的替代者:交叉注意力Transformer(CAT) 在深度学习的世界里,Transformer架构以其在自然语言处理(NLP)领域的卓越表现而闻名。然而,当它进入计算机视觉(CV)领域时,却面临着计算成本高昂和推理速度慢的双重挑战。现在,一项革命性的创新——交叉注意…

Qualcomm QCS6490 开发板运行高通AI Hub图像分类程序

相关代码可以在如下链接下载&#xff1a; ai-hub-models/apps/android/ImageClassification at main quic/ai-hub-models GitHub 所用硬件有&#xff1a; 1. UBUNTU20.04 2. 高通QCS6490 开发板 对下载下来的代码进行编译 1. ubuntu环境配置 1. python环境配置 如果你…

[SAP ABAP] 子例程

子例程 示例1 主程序(Z437_TEST_2024) INCLUDE文件(Z437_TEST_2024_F01) 输出结果如下所示 示例2 主程序(Z437_TEST_2024) INCLUDE文件(Z437_TEST_2024_F01) 输出结果如下所示 补充扩展练习 主程序(Z437_TEST_2024) INCLUDE文件(Z437_TEST_2024_F01) 输出结果如下所示 提示…

odoo 物联网 设备数据采集方案

图一 架构手稿(许老师专属) 图二 架构简图 部署 方案一&#xff1a; odoo业务数据库与设备采集数据库使用一个instance。 缺点&#xff1a;重启pg服务相互影响。 方案二&#xff1a; odoo业务数据库与设备采集数据库独立部署&#xff0c;使用两个instance。 优点&#xff1a;…

RedHat / CentOS安装FTP服务

本章教程,记录在RedHat / CentOS中安装FTP的具体步骤。FTP默认端口:21 1、安装 epel 源 yum install -y epel-release2、安装 pure-ftpd yum -y install pure-ftpd3、修改默认配置 # 默认配置位于 /etc/pure-ftpd/pure-ftpd.conf,在配置文件中找到下面几个参数进行修改:#…

AI视频生成技术爆发 引领虚拟数字人产业新潮流

2024年刚开局&#xff0c;先有OpenAI的AI视频生成模型Sora惊艳全网&#xff0c;随后阿里巴巴发布EMO&#xff0c;一张照片音频&#xff0c;就能生成具有生动表情和各种头部姿势、口型完全匹配高保真的人声头像动态视频。 技术的革新不仅为内容创作者打开了新世界的大门&#xf…

数据结构——队列练习题

在C语言中&#xff0c;.和->运算符用于访问结构体的成员变量。它们之间的区别在于&#xff1a;.运算符用于访问结构体变量的成员。->运算符用于访问结构体指针变量的成员 1a&#xff08;rear指向队尾元素后一位&#xff0c;判空判满时牺牲一个存储单元&#xff09; 首先…

PEFT - 安装及简单使用

LLM、AIGC、RAG 开发交流裙&#xff1a;377891973 文章目录 一、关于 PEFT二、安装1、使用 PyPI 安装2、使用源码安装 三、快速开始1、训练2、保存模型3、推理4、后续步骤 本文翻译整理自&#xff1a;https://huggingface.co/docs/peft/index 一、关于 PEFT &#x1f917;PEFT…

BugKu-WEB-sodirty

目录 前言 正文 信息收集 代码审计 验证 结尾 前言 七月始,暑假副本也正式开启 正文 信息收集 看着貌似没啥意义 看样子是有备份文件 下载下来 快速审计一下 代码审计 来吧 app.js没啥东西,主要是功能是实现error 我们找一找有没有index.js 找到了 \www\routes\in…

使用 Git Hooks 防止敏感信息泄露

欢迎关注公众号&#xff1a;冬瓜白 在日常开发中&#xff0c;我们可能会不小心将敏感信息提交到 Git。为了防止这种情况&#xff0c;可以利用 Git Hooks 编写一个简单的脚本&#xff0c;当发现提交中包含敏感词时&#xff0c;给出提示。 以下是一个基于 pre-commit 钩子的示例…

踩坑:Unity导出WebGL发布到手机上竖屏时强制显示横屏

具体的适配问题 公司的项目需要将游戏导出WebGL 发布到Web平台 本以为是个很简单的事情 谁知道却被个横竖屏适配搞的头晕 毕竟只有大学浅浅的学了下HTML这门语言 出来工作后基本上都是在跟C# Lua打交道 言归正传 看看具体问题吧 游戏如果从横屏进入 基本上不会有什么适配问题…

Finding Global Homophily in Graph Neural Networks When Meeting Heterophily

本文发表于:ICML22 推荐指数: #paper/⭐⭐⭐ 问题背景: 异配图的邻接矩阵难以确定,以及异配图的计算复杂度开销大 可行的解决办法:高通滤波多跳邻居,GPRGNN(pagerank一类&#xff0c;各阶邻居的权重不同,ACM-GCN&#xff08;高低通滤波,H2GCN&#xff08;应该复杂度很大&…

碳课堂|搞清楚碳足迹,只看这篇文章就够了

碳足迹管理是碳达峰碳中和的重要政策工具&#xff0c;2023年12月&#xff0c;国家发展改革委、工信部、国家市场监管总局、住房城乡建设部、交通运输部等部门联合印发《关于加快建立产品碳足迹管理体系的意见》&#xff0c;对产品碳足迹管理各项重点任务作出系统部署。 推动碳…

[leetcode]文件组合

. - 力扣&#xff08;LeetCode&#xff09; class Solution { public:vector<vector<int>> fileCombination(int target) {vector<vector<int>> vec;vector<int> res;int sum 0, limit (target - 1) / 2; // (target - 1) / 2 等效于 target /…

Windows 11内置一键系统备份与还原 轻松替代Ghost

面对系统崩溃、恶意软件侵袭或其他不可预见因素导致的启动失败&#xff0c;Windows 7~Windows 11内置的系统映像功能能够迅速将您的系统恢复至健康状态&#xff0c;确保工作的连续性和数据的完整性。 Windows内置3种备份策略 U盘备份&#xff1a;便携且安全 打开“创建一个恢…

Ubuntu20.04突然没网的一种解决办法

本来要学一下点云地图处理&#xff0c;用octomap库&#xff0c;但是提示少了octomap-server库&#xff0c;然后通过下面命令安装的时候&#xff1a; sudo apt install ros-noetic-octomap-server 提示&#xff1a;错误:7 https://mirrors.ustc.edu.cn/ubuntu focal-security …

MWC上海展 | 创新微MinewSemi携ME54系列新品亮相Nordic展台

6月28日&#xff0c; 2024MWC上海圆满落幕&#xff0c;此次盛会吸引了来自全球124个国家及地区的近40,000名与会者。本届大会以“未来先行&#xff08;Future First&#xff09;”为主题&#xff0c;聚焦“超越5G”“人工智能经济”“数智制造”三大子主题&#xff0c;探索讨论…

溶解氧(DO)理论指南(1)

转载自梅特勒官网资料&#xff0c;仅用于学习交流&#xff0c;侵权则删&#xff01; 溶解氧理论指南 1 溶解氧(DO)原理1.1 溶解氧和分压1.2 氧气在水中的溶解度1.3 溶解氧对生物的重要性1.4 溶解氧对工业的重要性 1 溶解氧(DO)原理 氧是宇宙中第三大常见元素&#xff0c;也是…

JavaScript(6)——数据类型转换

为什么需要类型转换&#xff1f; JavaScript是弱数据类型&#xff1a;JavaScript不知道变量到底属于哪种数据类型&#xff0c;只有赋值了才清除 使用表单&#xff0c;prompt获取的数据默认为字符串类型&#xff0c;此时不能直接进行算数运算 隐式转换 某些运算符被执行时&am…

两次叛国投敌,没有祸及子孙反而家族长盛不衰的传奇

这个人就是韩国国王韩王信&#xff0c;汉朝八大异姓王之一。 第一次叛国投敌&#xff0c;发生在楚汉争霸时期。有一次他的军队被项羽包围&#xff0c;于是选择了投降。不过&#xff0c;这是权宜之计&#xff0c;不久就借机回到刘邦阵营。 第二次叛国投敌&#xff0c;发生在西…