CUDA-共享内存法实现矩阵乘法(比常规方案提速一倍)

news2024/12/25 0:40:29

作者:翟天保Steven
版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处

共享内存是什么?

       共享内存是在多个处理单元之间共享数据的一种内存区域。在计算机体系结构中,共享内存通常指的是多个处理器核心或线程之间可以直接访问的内存区域。这些处理单元可以是在同一片硅芯片上的多个处理器核心,也可以是在多个硬件设备上运行的并行线程。

       共享内存在并行计算中扮演着重要的角色,因为它允许多个处理单元之间快速共享数据,而无需通过显式的通信机制。这种直接的共享机制可以大大提高并行计算的效率和性能。

       在GPU编程中,共享内存通常是在GPU的多个线程之间共享的硬件缓存区域。每个线程块(block)内的线程可以访问共享内存中存储的数据,并且这些数据对于同一个线程块内的所有线程都是可见的。共享内存通常具有更高的带宽和更低的访问延迟,因此可以用来加速数据的读取和写入操作。

       共享内存的使用可以提高并行计算的性能,并减少通信开销,特别是在需要大量数据共享的情况下,如矩阵乘法、图像处理等算法中。因此,共享内存是许多并行计算框架和编程模型中的重要组成部分。

常规算法

       在讲解共享内存法之前先回顾常规算法,常规算法通过分配区block和线程thread,使矩阵计算过程拆解,进而达到并行目的。

// 常规算法的核
__global__ void vectorMulGPU(const float* a, const float* b, float* c, int size)
{
	int row = blockIdx.y * blockDim.y + threadIdx.y;
	int col = blockIdx.x * blockDim.x + threadIdx.x;

	if (row < size && col < size)
	{
		float sum = 0.0f;
		for (int k = 0; k < size; ++k)
		{
			sum += a[row * size + k] * b[k * size + col];
		}
		c[row * size + col] = sum;
	}
}

共享内存法

       矩阵计算中(row0,col0)的值,等于前一个矩阵对应行row0和后一个矩阵对应列col0的数据点乘之和;同理,在(row0,col1)的计算中,又读取了一次row0的数据信息;那么如何将这两个读取类似数据的过程进行优化,便是共享内存法的意义所在。

       下面是共享内存法的核函数。

// 共享内存法的核
__global__ void vectorMulGPU2(const float* a, const float* b, float* c, int size)
{
	__shared__ float as[TILE_WIDTH][TILE_WIDTH];
	__shared__ float bs[TILE_WIDTH][TILE_WIDTH];

	// 索引
	int bx = blockIdx.x;
	int by = blockIdx.y;
	int tx = threadIdx.x;
	int ty = threadIdx.y;

	// 行列
	int row = by * TILE_WIDTH + ty;
	int col = bx * TILE_WIDTH + tx;

	// 遍历不同块
	float temp = 0.0;
	for (int k = 0; k < size / TILE_WIDTH; k++)
	{
		// 在同一块中的不同线程会计算各自数据,并存在共享存储中互相使用
		as[ty][tx] = a[row * size + k * TILE_WIDTH + tx];
		bs[ty][tx] = b[(k * TILE_WIDTH + ty) * size + col];
		__syncthreads(); // 等待块中其他线程同步
		// 将对应数据计算
		for (int m = 0; m < TILE_WIDTH; m++)
			temp += as[ty][m] * bs[m][tx];
		__syncthreads(); // 等待其他线程,如果不加这一句,部分线程中的as和bs会被后续循环的计算覆盖,进而导致结果错误
	}
	c[row * size + col] = temp;
}

       逐步解释代码:

  1. vectorMulGPU2是一个 GPU 的核函数,用于执行向量相乘操作。它接受三个参数:两个输入向量 ab,一个输出向量 c,以及向量的大小 size

  2. asbs这两行定义了共享内存中的两个二维数组,用于存储输入向量的部分数据。

  3. 接下来是一系列的变量定义和计算,其中涉及到了线程、块、行、列等概念的索引计算,以及关于如何在输入矩阵中定位数据的计算。

  4. for (int k = 0; k < size / TILE_WIDTH; k++)这个循环迭代地处理输入向量,将其分割成大小为 TILE_WIDTH 的小块。

  5. 在循环中,首先将当前处理的小块数据从输入向量 ab 中加载到共享内存中的 asbs 数组中,每个线程负责加载一部分数据。

  6. __syncthreads()这是一个同步函数,用于确保所有线程都已经加载完数据,才能继续执行下一步计算。

  7. 然后,在加载完数据后,每个线程会计算部分结果,并将其叠加在 temp 变量中,这个过程利用了共享内存中的数据。

  8. 再次使用同步函数确保所有线程都完成了计算,以免后续的数据加载覆盖了当前线程的计算结果。

  9. 最后,将计算得到的结果写入到输出向量 c 中。

       相较常规算法,共享内存法速度之所以提升,在于如下几点:

  1. 共享内存(Shared Memory):在CUDA编程中,共享内存是每个线程块共享的内存空间,它的读写速度比全局内存快得多。这段代码中使用了__shared__修饰符定义了asbs两个二维数组,用于存储每个线程块所需的部分输入数据。

  2. 矩阵块计算(Matrix Tile Computation):这段代码将整个矩阵分割成小块,每个线程块负责处理一个小块的计算。这样做的好处是利用了共享内存的局部性原理,减少了全局内存的访问次数,提高了内存访问效率。

  3. 线程同步(Thread Synchronization):在每个矩阵块计算完毕后,使用__syncthreads()函数进行线程同步,确保所有线程都完成了对共享内存的写入操作,然后再进行下一步的计算。这样可以避免数据竞争和不一致性。

  4. 数据重用(Data Reuse):由于每个线程块的计算结果都存储在共享内存中,可以重复利用这些数据进行后续计算,减少了对全局内存的访问次数,提高了数据重用率。

  5. 数据局部性(Data Locality):通过共享内存和矩阵块计算,每个线程块都在处理相邻的数据片段,这样可以提高数据局部性,减少了数据的跨越和访问延迟。

改进共享内存法

       如果你观察细致,那你必定会注意到如果矩阵行列数不是分块尺寸的整数倍,则上述代码便会出错,它会读取不在原计算序列中的数据,进而导致计算错误。假设矩阵是64*64,那单列或者单行可以分块为4个16,刚刚好,如果矩阵是70*70,那除了4个16外,还会余出6个数据没有参与运算。

       接下来对算法进行改进。

// 共享内存法(改进)的核
__global__ void vectorMulGPU3(const float* a, const float* b, float* c, int size)
{
	__shared__ float as[TILE_WIDTH][TILE_WIDTH];
	__shared__ float bs[TILE_WIDTH][TILE_WIDTH];

	// 索引
	int bx = blockIdx.x;
	int by = blockIdx.y;
	int tx = threadIdx.x;
	int ty = threadIdx.y;

	// 行列
	int row = by * TILE_WIDTH + ty;
	int col = bx * TILE_WIDTH + tx;

	// 遍历不同块
	float temp = 0.0;
	for (int k = 0; k < ceil(float(size) / TILE_WIDTH); k++)
	{
		// 在同一块中的不同线程会计算各自数据,并存在共享存储中互相使用
		if ((k * TILE_WIDTH + tx) < size && row < size)
		{
			as[ty][tx] = a[row * size + k * TILE_WIDTH + tx];
		}
		else
		{
			as[ty][tx] = 0.0f;
		}
		if ((k * TILE_WIDTH + ty) < size && col < size)
		{
			bs[ty][tx] = b[(k * TILE_WIDTH + ty) * size + col];
		}
		else
		{
			bs[ty][tx] = 0.0f;
		}
		__syncthreads(); // 等待块中其他线程同步
		if (row < size && col < size)
		{
			// 将对应数据计算
			for (int m = 0; m < TILE_WIDTH; m++)
				temp += as[ty][m] * bs[m][tx];
		}
		__syncthreads(); // 等待其他线程,如果不加这一句,部分线程中的as和bs会被后续循环的计算覆盖,进而导致结果错误
	}
	if (row < size && col < size)
	{
		c[row * size + col] = temp;
	}
}

       改进的几点如下:

  1. 循环终止条件

    • 原算法中,循环终止条件是 k < size / TILE_WIDTH,这意味着它只会遍历足够数量的块以处理整个矩阵。
    • 改进的算法中,循环终止条件是 k < ceil(float(size) / TILE_WIDTH),这使得它处理了多余的块。这样做是为了确保即使矩阵的大小不是块大小的整数倍时,也能正确处理。
  2. 数据加载

    • 在原算法中,数据加载时没有进行边界检查。这可能导致一些线程加载了超出数组边界的数据,造成错误的计算。
    • 改进的算法在加载数据时,会检查当前索引是否超出数组边界,如果超出则将对应元素置为0。这样可以避免超出边界的数据加载,确保计算的正确性。
  3. 结果存储

    • 原算法和改进的算法都会在计算完毕后将结果存储到全局内存中。但是在改进的算法中,存储结果时也进行了边界检查,以避免将结果存储到超出数组边界的位置。
  4. 性能优化

    • 改进的算法中,对于不在矩阵边界内的线程,避免了不必要的计算。这样可以减少不必要的计算量,提高了性能。

C++完整代码

Test.cuh

#ifndef MUL_H
#define MUL_H

#define TILE_WIDTH 16

void warmupCUDA();
void vectorMulCUDA(const float* a, const float* b, float* c, int N);
void vectorMulCUDA2(const float* a, const float* b, float* c, int N);
void vectorMulCUDA3(const float* a, const float* b, float* c, int N);

#endif // MUL_H

Test.cu

#include <vector>
#include <cuda_runtime.h>
#include <iostream>
#include "Test.cuh"

// 常规算法的核
__global__ void vectorMulGPU(const float* a, const float* b, float* c, int size)
{
	int row = blockIdx.y * blockDim.y + threadIdx.y;
	int col = blockIdx.x * blockDim.x + threadIdx.x;

	if (row < size && col < size)
	{
		float sum = 0.0f;
		for (int k = 0; k < size; ++k)
		{
			sum += a[row * size + k] * b[k * size + col];
		}
		c[row * size + col] = sum;
	}
}

// 共享内存法的核
__global__ void vectorMulGPU2(const float* a, const float* b, float* c, int size)
{
	__shared__ float as[TILE_WIDTH][TILE_WIDTH];
	__shared__ float bs[TILE_WIDTH][TILE_WIDTH];

	// 索引
	int bx = blockIdx.x;
	int by = blockIdx.y;
	int tx = threadIdx.x;
	int ty = threadIdx.y;

	// 行列
	int row = by * TILE_WIDTH + ty;
	int col = bx * TILE_WIDTH + tx;

	// 遍历不同块
	float temp = 0.0;
	for (int k = 0; k < size / TILE_WIDTH; k++)
	{
		// 在同一块中的不同线程会计算各自数据,并存在共享存储中互相使用
		as[ty][tx] = a[row * size + k * TILE_WIDTH + tx];
		bs[ty][tx] = b[(k * TILE_WIDTH + ty) * size + col];
		__syncthreads(); // 等待块中其他线程同步
		// 将对应数据计算
		for (int m = 0; m < TILE_WIDTH; m++)
			temp += as[ty][m] * bs[m][tx];
		__syncthreads(); // 等待其他线程,如果不加这一句,部分线程中的as和bs会被后续循环的计算覆盖,进而导致结果错误
	}
	c[row * size + col] = temp;
}

// 共享内存法(改进)的核
__global__ void vectorMulGPU3(const float* a, const float* b, float* c, int size)
{
	__shared__ float as[TILE_WIDTH][TILE_WIDTH];
	__shared__ float bs[TILE_WIDTH][TILE_WIDTH];

	// 索引
	int bx = blockIdx.x;
	int by = blockIdx.y;
	int tx = threadIdx.x;
	int ty = threadIdx.y;

	// 行列
	int row = by * TILE_WIDTH + ty;
	int col = bx * TILE_WIDTH + tx;

	// 遍历不同块
	float temp = 0.0;
	for (int k = 0; k < ceil(float(size) / TILE_WIDTH); k++)
	{
		// 在同一块中的不同线程会计算各自数据,并存在共享存储中互相使用
		if ((k * TILE_WIDTH + tx) < size && row < size)
		{
			as[ty][tx] = a[row * size + k * TILE_WIDTH + tx];
		}
		else
		{
			as[ty][tx] = 0.0f;
		}
		if ((k * TILE_WIDTH + ty) < size && col < size)
		{
			bs[ty][tx] = b[(k * TILE_WIDTH + ty) * size + col];
		}
		else
		{
			bs[ty][tx] = 0.0f;
		}
		__syncthreads(); // 等待块中其他线程同步
		if (row < size && col < size)
		{
			// 将对应数据计算
			for (int m = 0; m < TILE_WIDTH; m++)
				temp += as[ty][m] * bs[m][tx];
		}
		__syncthreads(); // 等待其他线程,如果不加这一句,部分线程中的as和bs会被后续循环的计算覆盖,进而导致结果错误
	}
	if (row < size && col < size)
	{
		c[row * size + col] = temp;
	}
}

// 预准备过程
void warmupCUDA() 
{
	float *dummy_data;
	cudaMalloc((void**)&dummy_data, sizeof(float));
	cudaFree(dummy_data);
}

// 常规算法
void vectorMulCUDA(const float* a, const float* b, float* c, int size)
{
	float *d_a, *d_b, *d_c;
	cudaMalloc((void**)&d_a, size * size * sizeof(float));
	cudaMalloc((void**)&d_b, size * size * sizeof(float));
	cudaMalloc((void**)&d_c, size * size * sizeof(float));

	cudaMemcpy(d_a, a, size * size * sizeof(float), cudaMemcpyHostToDevice);
	cudaMemcpy(d_b, b, size * size * sizeof(float), cudaMemcpyHostToDevice);

	dim3 threadsPerBlock(TILE_WIDTH, TILE_WIDTH);
	dim3 blocksPerGrid((size + threadsPerBlock.x - 1) / threadsPerBlock.x, (size + threadsPerBlock.y - 1) / threadsPerBlock.y);
	vectorMulGPU << <blocksPerGrid, threadsPerBlock >> > (d_a, d_b, d_c, size);
	cudaMemcpy(c, d_c, size * size * sizeof(float), cudaMemcpyDeviceToHost);

	cudaFree(d_a);
	cudaFree(d_b);
	cudaFree(d_c);
}

// 共享内存法
void vectorMulCUDA2(const float* a, const float* b, float* c, int size)
{
	float *d_a, *d_b, *d_c;
	cudaMalloc((void**)&d_a, size * size * sizeof(float));
	cudaMalloc((void**)&d_b, size * size * sizeof(float));
	cudaMalloc((void**)&d_c, size * size * sizeof(float));

	cudaMemcpy(d_a, a, size * size * sizeof(float), cudaMemcpyHostToDevice);
	cudaMemcpy(d_b, b, size * size * sizeof(float), cudaMemcpyHostToDevice);

	dim3 threadsPerBlock(TILE_WIDTH, TILE_WIDTH);
	dim3 blocksPerGrid((size + threadsPerBlock.x - 1) / threadsPerBlock.x, (size + threadsPerBlock.y - 1) / threadsPerBlock.y);
	vectorMulGPU2 << <blocksPerGrid, threadsPerBlock >> > (d_a, d_b, d_c, size);
	cudaMemcpy(c, d_c, size * size * sizeof(float), cudaMemcpyDeviceToHost);

	cudaFree(d_a);
	cudaFree(d_b);
	cudaFree(d_c);
}

// 共享内存法改进
void vectorMulCUDA3(const float* a, const float* b, float* c, int size)
{
	float *d_a, *d_b, *d_c;
	cudaMalloc((void**)&d_a, size * size * sizeof(float));
	cudaMalloc((void**)&d_b, size * size * sizeof(float));
	cudaMalloc((void**)&d_c, size * size * sizeof(float));

	cudaMemcpy(d_a, a, size * size * sizeof(float), cudaMemcpyHostToDevice);
	cudaMemcpy(d_b, b, size * size * sizeof(float), cudaMemcpyHostToDevice);

	dim3 threadsPerBlock(TILE_WIDTH, TILE_WIDTH);
	dim3 blocksPerGrid((size + threadsPerBlock.x - 1) / threadsPerBlock.x, (size + threadsPerBlock.y - 1) / threadsPerBlock.y);
	vectorMulGPU3 << <blocksPerGrid, threadsPerBlock >> > (d_a, d_b, d_c, size);
	cudaMemcpy(c, d_c, size * size * sizeof(float), cudaMemcpyDeviceToHost);

	cudaFree(d_a);
	cudaFree(d_b);
	cudaFree(d_c);
}

main.cpp

#include <iostream>
#include <vector>
#include <time.h>
#include "Test.cuh"

using namespace std;

void vectorMulCPU(const std::vector<float>& a, const std::vector<float>& b, std::vector<float>& c, int size)
{
#pragma omp parallel for
	for (int i = 0; i < size; ++i)
	{
		for (int j = 0; j < size; ++j)
		{
			float sum = 0.0f;
			for (int k = 0; k < size; ++k)
			{
				sum += a[i * size + k] * b[k * size + j];
			}
			c[i * size + j] = sum;
		}
	}
}

int main()
{
	// 乘法测试
	const int N = 1000; // 矩阵大小
	std::vector<float> a0(N * N, 0.0f);
	std::vector<float> b0(N * N, 0.0f);
	std::vector<float> c0(N * N, 0.0f);
	for (int i = 0; i < N; ++i)
	{
		for (int j = 0; j < N; ++j)
		{
			a0[i * N + j] = float(rand() % 255);
			b0[i * N + j] = float(rand() % 255);
		}
	}
	std::vector<float> a1 = a0;
	std::vector<float> b1 = b0;
	std::vector<float> c1 = c0;
	std::vector<float> a2 = a0;
	std::vector<float> b2 = b0;
	std::vector<float> c2 = c0;
	std::vector<float> a3 = a0;
	std::vector<float> b3 = b0;
	std::vector<float> c3 = c0;
	// CPU矩阵乘法
	clock_t sc, ec;
	sc = clock();
	vectorMulCPU(a0, b0, c0, N);
	ec = clock();
	cout << "mul CPU time:" << float(ec - sc) / 1000 << endl;
	// 准备工作
	bool flag = true;
	clock_t sw, ew;
	sw = clock();
	warmupCUDA();
	ew = clock();
	cout << "warmup time:" << float(ew - sw) / 1000 << endl;
	// GPU矩阵乘法
	clock_t sg1, eg1;
	sg1 = clock();
	vectorMulCUDA(a1.data(), b1.data(), c1.data(), N);
	eg1 = clock();
	cout << "mul GPU time:" << float(eg1 - sg1) / 1000 << endl;
	// 检查结果
	for (int i = 0; i < N * N; ++i)
	{
		if (c0[i] != c1[i])
		{
			std::cerr << "mul GPU Error: Incorrect result at index " << i << std::endl;
			flag = false;
			break;
		}
	}
	// GPU矩阵乘法2
	clock_t sg2, eg2;
	sg2 = clock();
	vectorMulCUDA2(a2.data(), b2.data(), c2.data(), N);
	eg2 = clock();
	cout << "mul GPU2 time:" << float(eg2 - sg2) / 1000 << endl;
	// 检查结果
	for (int i = 0; i < N * N; ++i)
	{
		if (c0[i] != c2[i])
		{
			std::cerr << "mul GPU2 Error: Incorrect result at index " << i << std::endl;
			flag = false;
			break;
		}
	}
	// GPU矩阵乘法3
	clock_t sg3, eg3;
	sg3 = clock();
	vectorMulCUDA3(a3.data(), b3.data(), c3.data(), N);
	eg3 = clock();
	cout << "mul GPU3 time:" << float(eg3 - sg3) / 1000 << endl;
	// 检查结果
	for (int i = 0; i < N * N; ++i)
	{
		if (c0[i] != c3[i])
		{
			std::cerr << "mul GPU3 Error: Incorrect result at index " << i << std::endl;
			flag = false;
			break;
		}
	}
	// 输出结果
	if (flag)
	{
		std::cout << "successful!" << std::endl;
	}
	else
	{
		std::cout << "error!" << std::endl;
	}
	return 0;
}

测试效果

       在测试代码中有一段是warmup,这是GPU预准备过程,用于执行一些额外开销,如果不进行这一步直接运行后续算法,则会耗时变长,干扰判断。

       相较常规算法,共享内存法提升了约1倍,而改进的共享内存法面对矩阵1000*1000的计算,得到了正确的结果,共享内存法提示错误。如果你把矩阵尺寸改为16的整数倍,则共享内存法也可以顺利通过。

       如果文章帮助到你了,可以点个赞让我知道,我会很快乐~加油!

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

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

相关文章

dPEG与传统PEG以及其他烷基交联剂产品的优势

作为Linker的dPEG 研究证明&#xff0c;通过交联剂将不同物质结合在一起的能力已被证明是诊断和药物输送系统中非常有用的一项技术。由多分散PEG组成的交联剂已被用于制备多种多聚物以及将靶向配体偶联到纳米颗粒上。这通常用于需要非常大的尺寸以提供良好的DMPK性能并且受多分…

GPU术语

SP(Streaming Processor)流处理器 流处理器是GPU最基本的处理单元&#xff0c;在fermi架构开始被叫做CUDA core。 SM(Streaming MultiProcessor) 一个SM由多个CUDA core组成。SM还包括特殊运算单元(SFU)&#xff0c;共享内存(shared memory)&#xff0c;寄存器文件(Register …

抖音短视频矩阵系统技术源头/源代码开发部署/SaaS贴牌/源码api代开发

抖音短视频矩阵系统技术源头/源代码开发部署/SaaS贴牌/源码官方平台api授权代开发 一、短视频矩阵系统源码开发步骤 短视频矩阵系统的源头开发步骤通常包括以下几个关键阶段&#xff1a; 1.需求分析&#xff1a;明确系统的目标用户、功能需求、性能要求等。 2.系统设计&…

加密技术在保护企业数据中的应用

加密技术是企业数据保护的核心&#xff0c;对于维护信息安全至关重要。透明加密技术使文件加密后不改变用户对文件的使用习惯&#xff0c;内部文件打开自动解密&#xff0c;存储自动加密&#xff0c;一旦离开使用环境&#xff0c;加密文件将无法正常读取&#xff0c;从而保护文…

【算法】唯一分解定理及最lcm和gcd关系 宝石组合

前言 今天在做一道宝石组合的题目时了解到了这个定理&#xff0c;还是蛮有意思的。 思想 唯一分解定理&#xff1a; 对于任何正整数n&#xff0c;有 n p 1 a 1 p 2 a 2 . . . p k a k n p_1^{a1} \times p_2^{a2} \times ... \times p_k^{ak} np1a1​p2a2​...pkak​ …

【最大公约数 并集查找 调和级数】1998. 数组的最大公因数排序

本文涉及知识点 最大公约数 并集查找 调和级数 LeetCode1998. 数组的最大公因数排序 给你一个整数数组 nums &#xff0c;你可以在 nums 上执行下述操作 任意次 &#xff1a; 如果 gcd(nums[i], nums[j]) > 1 &#xff0c;交换 nums[i] 和 nums[j] 的位置。其中 gcd(nums…

【JVM】类加载机制及双亲委派模型

目录 一、类加载过程 1. 加载 2. 连接 a. 验证 b. 准备 c. 解析 3. 初始化 二、双亲委派模型 类加载器 双亲委派模型的工作过程 双亲委派模型的优点 一、类加载过程 JVM的类加载机制是JVM在运行时&#xff0c;将 .class 文件加载到内存中并转换为Java类的过程。它…

Android 桌面小组件 AppWidgetProvider

Android 桌面小组件 AppWidgetProvider 简介 小组件就是可以添加到手机桌面的窗口。点击窗口可以进入应用或者进入应用的某一个页面。 widget 组件 如需创建 widget&#xff0c;您需要以下基本组件&#xff1a; AppWidgetProviderInfo 对象 描述 widget 的元数据&#xff0…

Linux基础配置(镜像挂载,FQDN)

CentOS基础配置&#xff1a; 以下是appsrv的基础配置脚本&#xff0c;CentOS系统只需要把appsrv改成需要的主机名即可&#xff08;因为Linux基础配置都差不多&#xff0c;写脚本是最省时间的做法&#xff0c;IP地址的配置一般用nmtui图形化界面工具&#xff09; #!/bin/bash …

告别杂乱桌面,开启纯净视界!DeskCover Pro,Mac用户的桌面神器!

DeskCover Pro for Mac是一款专为macOS设计的桌面图标隐藏软件&#xff0c;其主要功能和特点包括&#xff1a; 桌面图标隐藏&#xff1a;通过单击鼠标或按全局热键&#xff0c;可以快速隐藏桌面上的所有图标&#xff0c;为您提供一个干净整洁的工作环境。窗口聚焦&#xff1a;…

ChatGPT-Next-Web漏洞利用分析(CVE-2023-49785)

1. 漏洞介绍 ​ 日常网上冲浪&#xff0c;突然粗看以为是有关Chat-GPT的CVE披露出来了&#xff0c;但是仔细一看原来是ChatGPT-Next-Web的漏洞。漏洞描述大致如下&#xff1a;&#xff08;如果有自己搭建了还没更新的速速修复升级防止被人利用&#xff0c;2.11.3已经出来了&am…

MySQL—多表设计与查询

目录 多表设计 ▐ 数据库设计范式 ▐ 多对一 关系表设计 ▐ 多对多 关系表设计 关联查询 ▐ 概述 ▐ 内连接 ○ 思考&#xff1f; ▐ 左外连接 ▐ 右外连接 多表设计 ▐ 数据库设计范式 • 第一范式&#xff1a;确保每列保持原子性 ( 列不可再分解 ) 例如联系方式…

Mac数据恢复软件快速比较:适用于Macbook的10佳恢复软件

数据丢失导致无数个人和组织每天损失大量资金。更糟糕的是&#xff0c;某些文件具有货币价值和情感意义&#xff0c;使它们不可替代&#xff0c;并使数据恢复成为唯一可行的选择。最好的消息是Mac用户可以从各种数据恢复程序中进行选择。为了帮助您尽可能快速、轻松地恢复丢失的…

弹性云服务器是什么,为何如此受欢迎

云计算作为当下炙手可热的技术领域&#xff0c;已然成为现代企业不可或缺的核心能力。云服务器作为云计算的基石之一&#xff0c;在这个数字化时代发挥着至关重要的作用。而弹性云服务器&#xff0c;作为云服务器的一种演进形式&#xff0c;更是备受瞩目。 弹性云服务器&#…

求知导刊-知网收录//旬刊//如何投稿?

求知导刊-知网收录//旬刊//如何投稿&#xff1f; 《求知导刊》栏目设置 理论探索、课堂教学、教改课改、教育管理、教师教育、教学案例、学科进展、学术论坛。 《求知导刊》征稿对象&#xff1a; 全国科技工作者、教育工作者&#xff0c;各级科技与教育部门的领导者以及管理…

鸿蒙内核源码分析(时间管理篇) | 谁是内核基本时间单位

时间概念太重要了&#xff0c;在鸿蒙内核又是如何管理和使用时间的呢? 时间管理以系统时钟 g_sysClock 为基础&#xff0c;给应用程序提供所有和时间有关的服务。 用户以秒、毫秒为单位计时.操作系统以Tick为单位计时&#xff0c;这个认识很重要. 每秒的tick大小很大程度上决…

AI绘画成果展(第一期)

免费获取更多原图&#xff0c;备注“AI绘画”&#xff0c;可在文章末尾点击名片进qun获取。 免费获取更多原图&#xff0c;备注“AI绘画”&#xff0c;可在文章末尾点击名片进qun获取。

Python密码测试程序

下面是一个简单的 Python 密码测试程序&#xff0c;用于检查用户输入的密码是否符合一些基本的安全要求&#xff0c;如长度、包含字母和数字等。这个程序可以作为一个基本的密码验证器&#xff0c;你可以根据需要进行修改和扩展。 1、问题背景 我们正在编写一个程序&#xff0…

花园牛奶:从靠谱奶牛到新鲜牛奶的匠心之旅

在花园乳业有限公司&#xff0c;我们深知生产出优质牛奶的秘诀——从靠谱的奶牛开始。为此&#xff0c;我们特意引进了品质卓越的荷斯坦奶牛&#xff0c;它们以“黑白花”的优雅身姿&#xff0c;成为了我们牧场上的明星。荷斯坦奶牛以其出色的生产性能和高产奶量而著称&#xf…

uniapp 自定义App UrlSchemes

需求&#xff1a;外部浏览器H5页面&#xff0c;跳转到uniapp开发的原生app内部。 1、uniapp内部的配置&#xff1a; &#xff08;1&#xff09;打开manifest->App常用其他设置&#xff0c;如下&#xff0c;按照提示输入您要设置的urlSchemes&#xff1a; &#xff08;2&am…