【csapp】cachelab

news2025/3/16 18:33:23

文章目录

      • Part A
      • Part B
        • 32 * 32
        • 64 * 64
        • 61 * 67

实验全程参考大佬的博客CS:APP3e 深入理解计算机系统_3e CacheLab实验 ,感觉大佬在矩阵转置那块介绍的还是有些简略,我自己又做了点动图加以补充理解。膜拜大佬!

Part A

先解决解析命令行参数的问题,文档推荐使用getopt()函数进行解析:

#include <unistd.h>
#include <stdlib.h>
#include <getopt.h>
int getopt(int argc, char *const argv[], const char *optstring);

着重关注第三个参数——选项字符串,如ab:c:de::就是一个选项字符串。对应到命令行就是-a ,-b ,-c ,-d, -e。冒号表示参数,一个冒号就表示这个选项后面必须带有参数,但是这个参数可以和选项连在一起写,也可以用空格隔开,比如-b123-b 123。两个冒号的就表示这个选项的参数是可选的,即可以有参数,也可以没有参数,有参数时,参数与选项之间不能有空格。如果后面不带冒号则说明没有选项参数。

其返回值就是对应选项字符的ascii码值,有选项参数,则参数字符串保存在extern char* optarg中。如果选项读完了就返回-1。更具体的信息可以使用man手册查看:man 3 getopt

我们要实现的例子中命令行参数有[-hv] -s <s> -E <E> -b <b> -t <tracefile>,所以对应的选项字符串就是hvs:E:b:t:

首先需要定义缓存数据结构,cache的结构如下:
image-20231220145511584

由于题目只关注hit/miss/eviction的次数,不需要实际存取数据,为了简便就不实现缓存块block了,如果实现的话就是添加一个指针,分配缓存空间的时候需要malloc分配2b个字节的空间。所以只需要validtag即可。

题目要求替换算法采用LRU,也就是eviction时替换最近没有被使用的line,所以需要添加一个记录最后访问时间的成员,每次访问的时候就更新一下。这里直接采用最简单粗暴的方式,用一个时间戳从0开始计数,每访问一次就+1,最小的则是访问时间最为久远的,进行替换。

由此可以定义数据类型:

typedef struct
{
    bool valid;
    unsigned long tag;
    unsigned long time;
}line_;

这就是line的类型,2e个行构成了一个组(set),为了方便再定义一个组类型:

typedef line_* set_;

组其实就是一个line数组,数组名是指针类型,后续方便malloc动态分配2e * sizeof(line_)的空间。

然后2s个组就构成了整个缓存结构,所以3为了方便再定义一个类型:

typedef set_* cache_;

和上面一样,这就是一个set数组,后续分配2s * sizeof(set_)的空间。

需要定义三个全局变量记录最终结果:

int hit;
int miss;
int eviction;

此外还需要定义几个全局变量,分别是-h选项对应的help信息,这个直接照抄示例程序的就行:

const char *help_message = 
"Usage: ./csim-ref [-hv] -s <num> -E <num> -b <num> -t <file>\n\
Options:\n\
	-h         Print this help message.\n\
	-v         Optional verbose flag.\n\
	-s <num>   Number of set index bits.\n\
	-E <num>   Number of lines per set.\n\
	-b <num>   Number of block offset bits.\n\
	-t <file>  Trace file.\n\n\
Examples:\n\
	linux>  ./csim-ref -s 4 -E 1 -b 4 -t traces/yi.trace\n\
	linux>  ./csim-ref -v -s 8 -E 2 -b 4 -t traces/yi.trace";

选项字符串:

const char *command_options = "hvs:E:b:t:";

要追踪的文件名:

FILE* tracefile = NULL;

缓存对象:

cache_ cache = NULL;

是否输入-v选项,也就是是否显示跟踪信息的可选详细标志默认为false,当命令行参数检测到v后置为true即可:

bool verbose = false;

还有几个记录缓存大小相关的数据:

unsigned long s = 0;
unsigned long b = 0;
unsigned long S = 0;
unsigned long E = 0;

其中2s是sets的数量;2b是每行中缓存块的数量,这里不关注;S就是块的数量,数值等于2s;E是每个set中的line数。

数据类型定义完成,开始逐步处理。首先是解析命令行参数,代码如下:

int checkCommand(int argc, char* argv[])
{
	char ch;
	while ((ch = getopt(argc, argv, command_options)) != -1)
	{
		switch(ch)
		{
		case 'h':
			printf("%s\n", help_message);
			exit(0);
		case 'v':
			verbose = true;
			break;
		case 's':
			s = atol(optarg);
			S = 1 << s;
			if (s <= 0)
			{
				printf("%s\n", help_message);
				exit(-1);
			}
			break;
		case 'E':
			E = atol(optarg);
			if (E <= 0)
			{
				printf("%s\n", help_message);
				exit(-1);
			}
			break;
		case 'b':
			b = atol(optarg);
			if (b <= 0)
			{
				printf("%s\n", help_message);
				exit(-1);
			}
			break;
		case 't':
			if ((tracefile = fopen(optarg, "r")) == NULL)
			{
				printf("%s\n", "Failed to open tracefile");
				exit(-1);
			}
			break;
		default:
			printf("%s\n", help_message);
			exit(-1);
		}
	}
	if (s == 0 || b ==0 || E == 0 || tracefile == NULL)
    {
        printf("%s\n", "Command line arguments are missing");
		printf("%s\n", help_message);
        exit(-1);
    }
}

while嵌套switch结构仅仅能判断输入的命令行参数中输入的值合法,但不能判断缺少命令行参数的情况。所以还需要加个if判断,确保命令行参数输入齐全。

接下来该给缓存对象分配空间了,一共需要开辟S个set,每个块中开辟E个line,每个line中就不需要开辟block了。我们此前定义的缓存对象为cache_ cache = NULL,其中cache_set_*类型,所以cache是一个set数组指针,每一个成员是一个set,一共有S个成员,所以需要给它分配S * sizeof(set_)个字节的空间,并且需要初始化为0。对于它的每一个成员,又是一个line数组,每个成员都有Eline,所以需要循环遍历给每个成员开辟E * sizeof(line_)个字节的空间:

void initCache()
{
	cache = calloc(S, sizeof(set_));
	if (cache == NULL)
	{
		printf("Failed to calloc set\n");
		exit(-1);
	}

	for (unsigned long i = 0; i < S; i++)
	{
		cache[i] = calloc(E, sizeof(line_));
		if (cache[i] == NULL)
		{
			printf("Failed to calloc line\n");
			exit(-1);
		}
	}
}

同时把free函数也写出来,开辟时从上至下开辟,释放时就要从下至上释放:

void freeCache()
{
	for (unsigned long i = 0; i < S; i++)
		free(cache[i]);
	free(cache);
}

接下来就是程序的核心部分,测试hit/miss/eviction的次数。

文件中I表示指令加载,我们要统计的是数据加载,所以遇到I开头的指令直接忽略,其余指令开头都有一个空格,以此区分。

如果命令是L(a data load)S(a data store),则需要判断一次是否命中或替换。如果命令是M(a data modify, a data load followed by a data store),则相当于一个L和一个M,需要进行两次判断。

判断用一个函数实现,那这个函数需要哪些参数呢?这就需要了解缓存读取规则了。
image-20231221135006231

首先进行组选择,也就是在哪个set中进行查找,通过提取addr对应图中的s bits位可以得到:(addr >> b) & ((1 << s) - 1)

首先addr >> b得到低s位为目标值,(1 << s) - 1得到低s位为全1,二者按位与即为目标值。

然后进行行匹配,这一步需要提取tagaddr >> (s + b)

所以测试函数就可以写出来:

void test()
{
	char op;
	unsigned long addr;
	int size;
	while (fscanf(tracefile, " %c %lx,%d", &op, &addr, &size) != EOF)
	{
		if (op == 'I')
			continue;
		if (verbose)
			printf("%c %lx ", op, addr);
		unsigned long set_index = (addr >> b) & ((1 << s) - 1);
		unsigned long tag = addr >> (b + s);
		if (op == 'L' || op == 'S')
			judge(set_index, tag);
		else if (op == 'M')
		{
			judge(set_index, tag);
			judge(set_index, tag);
		}
	}
}

接下来需要完善judge判断函数,判断每次访问是hit,还是miss,或是eviction

现在拿到了要查找的set的下标,只需要遍历set匹配tag即可。如果匹配上了并且validtrue,就是hit,更新时间戳,judge结束,直接返回即可。如果miss了,则需要找出上次访问时间最为久远的line,如果没数据则填充进去(因为没有实现block,所以直接将valid置为true即可),然后更新tag和时间戳,如果有数据则发生冲突,eviction++time值最小的line那个自然就是要替换的,然后更新time也要注意,这里有两种策略,一种是在遍历的同时把最大的time记录下来,+1就是要更新的值;或者设置一个全局变量计数。这里采用前者策略。

所以judge函数也就写出来了:

void judge(unsigned long set_index, unsigned long tag)
{
	for (unsigned long i = 0; i < E; i++)
	{
		if (cache[set_index][i].tag == tag && cache[set_index][i].valid)
		{
			if (verbose)
				printf("hit\n");
			hit++;
			cache[set_index][i].time++;
			return;
		}
	}

	if (verbose)
		printf("miss\n");
	miss++;

	// 找出时间戳最小的一个并记录下标,同时记录最大的时间戳
	unsigned long max = 0, min = -1, line_index = 0;
	for (unsigned long i = 0; i < E; i++)
	{
		if (cache[set_index][i].time <= min)
		{
			min = cache[set_index][i].time;
			line_index = i;
		}
		if (cache[set_index][i].time >= max)
			max = cache[set_index][i].time;
	}

	cache[set_index][line_index].time = max + 1;
	cache[set_index][line_index].tag = tag;

	if (cache[set_index][line_index].valid)
	{
		if (verbose)
			printf(" and eviction\n");
		eviction++;
	}
	else
	{
		if (verbose)
			printf("\n");
		cache[set_index][line_index].valid = true;
	}
}

拼凑一下就凑出了main函数:

int main(int argc, char* argv[])
{
	checkCommand(argc, argv);
	initCache();
	test();
	freeCache();
    printSummary(hit, miss, eviction);
    return 0;
}

测试一下,可以通过:
image-20231221182043065

Part B

以下均假设数组第一个元素映射到set[0][0] 为前提。

文档给了缓存的大小参数:s = 5, E = 1, b = 5,其实就是下面这样:
image-20231222131615475

一共有25 = 32个set,每个set有1个line,每个line中有25 = 32byte大小的block,可以存放32 / 4 = 8int类型的数据。所以cache最多存放32 * 8 = 256int类型的数据。那么当(&int1 - &int2) / 4 = 256 * k时,两个数据就会映射到同一个位置,也就是会发生冲突。

以32*32的矩阵进行举例,一行32个int需要占用4个set,所以缓存一次性最多存8行,也就是两个元素正好差八行的整数倍就会发生冲突,比如A[7][0] = A[0][0],二者会映射到同一个位置,会发生冲突。

不过这种冲突情况是不会发生的,因为A转置到B的两个元素并不会正好相差8*k行。

还有一个很隐藏的点,在文件tracegen.c文件中可以看到A、B数组原型为A[256][256],俩都是256 * 256的数组,A[0][0]B[0][0]正好差了256 * 256个数据,会映射到同一个位置!这个是很重要的,所以如果进行B[7][0] = A[0][0]这样的赋值也会发生冲突。因为B[7][0]A[7][0]映射到了一个位置。并且在转置过程中,对角线上的元素是不变的,也就是B[i][i] = A[i][i],如果直接赋值也必然会发生冲突,需要单独考虑。

32 * 32

下面尝试8 * 8进行矩阵分块,理解一下分块是如何减少不命中和冲突的,下图中每一个色块覆盖8*8的区域:
image-20231222142559268

缓存映射情况如下:
image-20231225233822719

以绿色色块为例:
image-20231222152539590

左侧对应A数组,右侧对应B数组。赋值语句为B[j][i] = A[i][j]。对应数据可以全部加载进缓存不冲突:
image-20231222154437644

这样虽然只同时利用了16个set,但是只会在加载分块进缓存的时候产生冲突,而如果使用更大的分块,在加载分块的时候必然会加载两个正好相差8行的元素,产生更多冲突。如果采用更小的分块,虽然没有增加冲突,但是缓存的利用率更低了。所以权衡一下8*8就是最优分块。

就用这种分块策略进行测试:

for (int i = 0; i < N; i += 8)
    for (int j = 0; j < M; j += 8)
        for (int m = i; m < i + 8; ++m)
            for (int n = j; n < j + 8; ++n)
                B[n][m] = A[m][n];

结果如下:
image-20231222171432116

与300次的要求还有差距,这就需要解决一下对角线上的元素冲突问题。以第一个白色块为例:
32优化前

A的第一行要赋值到B的第一列,首先读A[0][0],第一次肯定不命中,加载进来,set[0]A[0];然后读B[0][0],还是不在,加载进来,set[0]B[0];然后读A[0][1],不命中,加载进来,set[0]又存A[0],直到A的第一行全部读完。再读下一行,首先读A[1][0],不命中,加载进来,set[4]A[1];再读B[0][1],不命中,加载进来,set[0]B[0];再读A[1][1],不命中,加载进来,set[4]A[1];再读B[1][1],不命中,加载进来,set[4]B[1];再读A[1][2],不命中,加载进来,set[4]A[1],直到A的第二行全部读完,缓存都会命中。

以此类推,后面每一行相比第一行都多了2次不命中,一个对角块有8行,一块就会多14次不命中,一共有4块,总共就会多56次不命中。如果把这些解决了,理论上就能控制在300以内。

解决方法也很粗暴,直接定义8个局部变量,局部变量可以存放在寄存器中。依次把A的一行全部读出来,依次给B对应的一列赋值,这样可以避免中间多余的两次不命中:

for (int i = 0; i < N; i += 8)
    for (int j = 0; j < M; j += 8)
        for (int k = i; k < i + 8; k++)
        {
            int t0 = A[k][j];
            int t1 = A[k][j+1];
            int t2 = A[k][j+2];
            int t3 = A[k][j+3];
            int t4 = A[k][j+4];
            int t5 = A[k][j+5];
            int t6 = A[k][j+6];
            int t7 = A[k][j+7];
          
            B[j][k] = t0;
            B[j+1][k] = t1;
            B[j+2][k] = t2;
            B[j+3][k] = t3;
            B[j+4][k] = t4;
            B[j+5][k] = t5;
            B[j+6][k] = t6;
            B[j+7][k] = t7;
        }

测试如下:
image-20231222172425137

结果正好比预想的少56次。也达到了题目要求。

再看一下优化对角线后的读取情况加深理解:
32优化后

64 * 64

做完动图突然发现不合适的地方,右边为要转置到B中的某个分块,分块初始应该是白色的没有数据的,就当白色看吧。

此时数据一行有64个元素,8个set才能存满一行,cache存满最多能存4行。也就是两个元素中间差4行的整数倍就会发生冲突比如A[0][0]A[4][0]都会映射到cache[0][0]位置。

如果仍使用8 * 8分块,那么每个分块内部读取的时候都会发生冲突,比如A[0][0]A[4][0]都在一个分块内。如果使用4 * 4分块,那cache利用率就减半,结果肯定也不理想。

还是考虑8 * 8分块,把每一个块分成4部分:
image-20231224125552937

本来是要把黄色部分转置到黄色部分,灰色部分本应转置到绿色部分,但是绿色部分和黄色部分映射到同一块缓存,会发生冲突,先把灰色部分转置到灰色部分。下图颜色加深代表要访问某个元素,左边是A矩阵,右边是B矩阵:
64-1

为了避免对角线分块上会多miss的问题,还是采用先把A的一行全部读出来的策略。这样就以较少的miss把B矩阵初步填满,此时填满之后效果如下:
image-20231224140806659

再交换灰色块和绿色块,就完成了整个分块的转置。可惜的是这样优化仍拿不到满分。

进一步优化,在转换灰色块时逆序转换:
image-20231225221604066

过程如下:
64-2

整个过程并没有因为逆序存放发生多余的miss。

然后读绿色块和蓝色块时按列来读,从内到外:
64-3

从下向上按行转换B中的灰色块到绿色块:
64-4

此时可以发现,正好可以把用临时变量存放的A中的绿色块和蓝色块赋到B中:
image-20231225224500546

并且这个过程不会发生miss!复原灰色块的同时把绿色块和蓝色块也转置了。

继续后面的过程:
64-5

此时转置情况如下:
image-20231225225709941

剩下的就不列举了,都是一样的过程,代码如下:

for (int i = 0; i < N; i += 8)
{
    for (int j = 0; j < M; j += 8)
    {
        for (int k = i; k < i + 4; ++k)
        {
            // 读取A中的黄色块和灰色块
            int t0 = A[k][j];
            int t1 = A[k][j+1];
            int t2 = A[k][j+2];
            int t3 = A[k][j+3];
            int t4 = A[k][j+4];
            int t5 = A[k][j+5];
            int t6 = A[k][j+6];
            int t7 = A[k][j+7];

            // 黄色块在B中正常转置
            B[j][k] = t0;
            B[j+1][k] = t1;
            B[j+2][k] = t2;
            B[j+3][k] = t3;
            // 灰色块在B中灰色块位置逆序放置
            B[j][k+4] = t7;
            B[j+1][k+4] = t6;
            B[j+2][k+4] = t5;
            B[j+3][k+4] = t4;
        }
        for (int l = 0; l < 4; ++l)
        {
            // 由内到外按列读取A中绿色块和蓝色块
            int t0 = A[i+4][j+3-l];
            int t1 = A[i+5][j+3-l];
            int t2 = A[i+6][j+3-l];
            int t3 = A[i+7][j+3-l];
            int t4 = A[i+4][j+4+l];
            int t5 = A[i+5][j+4+l];
            int t6 = A[i+6][j+4+l];
            int t7 = A[i+7][j+4+l];

            // 从下至上按行转换B中的灰色块到绿色块
            B[j+4+l][i] = B[j+3-l][i+4];
            B[j+4+l][i+1] = B[j+3-l][i+5];
            B[j+4+l][i+2] = B[j+3-l][i+6];
            B[j+4+l][i+3] = B[j+3-l][i+7];
            // 将临时变量中存放的A中的绿色块和蓝色块B中的灰色块和蓝色块,完成转置
            B[j+3-l][i+4] = t0;
            B[j+3-l][i+5] = t1;
            B[j+3-l][i+6] = t2;
            B[j+3-l][i+7] = t3;
            B[j+4+l][i+4] = t4;
            B[j+4+l][i+5] = t5;
            B[j+4+l][i+6] = t6;
            B[j+4+l][i+7] = t7;
        } 
    }
}

测试通过:
image-20231225231124104

61 * 67

此时矩阵不是32或64的方阵,不会像之前一样隔4行或8行就映射到同一个缓存快,所以可以尝试一下依次利用更大的缓存,大胆一点全部用上,进行16 * 16分块,不过Aii和Bii还是会映射到同一个缓存快,还需要单独处理,用临时变量存放,代码如下:

for (int i = 0; i < N; i += 16)
{
    for (int j = 0; j < M; j += 16)
    {
        for (int k = i; k < i + 16 && k < N; ++k)
        {
            int temp_position = -1;
            int temp_value = 0;
            int l;
            for (l = j; l < j + 16 && l < M; ++l)
            {
                // 横坐标等于纵坐标,局部变量暂存,整个block读完再处理
                if (l == k)
                {
                    temp_position = k;
                    temp_value = A[k][k];
                }
                else
                    B[l][k] = A[k][l];
            }
            // 遇到了冲突元素
            if (temp_position != -1)
                B[temp_position][temp_position] = temp_value;
        }
    }
}

测试通过,正好卡在了2000线边缘:
image-20231225231927817

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

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

相关文章

fpga verilog rs232 发送模块实现

RS-232是一种串行通信协议&#xff0c;用于在计算机和其他外部设备之间进行数据传输。RS-232定义了电气特性、信号级别、机械特性和传输速率等规范&#xff0c;为串行通信提供了一种标准化的接口。 RS-232通常使用DB9连接器&#xff0c;用于传输和接收数据、控制信号以及地线连…

VMware17Pro虚拟机安装Linux CentOS 7.9(龙蜥)教程(超详细)

目录 1. 前言2. 下载所需文件3. 安装VMware3.1 安装3.2 启动并查看版本信息3.3 虚拟机默认位置配置 4. 安装Linux4.1 新建虚拟机4.2 安装操作系统4.2.1 选择 ISO 映像文件4.2.2 开启虚拟机4.2.3 选择语言4.2.4 软件选择4.2.5 禁用KDUMP4.2.6 安装位置配置4.2.7 网络和主机名配置…

小学班委有哪些职位

在成长的道路上&#xff0c;班委是一个不可或缺的角色。它不仅是一个职位&#xff0c;更是一份责任和担当。对于孩子们来说&#xff0c;成为班委不仅意味着荣誉&#xff0c;更意味着在集体中发挥自己的力量&#xff0c;为班级做贡献。 那么&#xff0c;小学班委有哪些职位呢&am…

小学班级管理方法和措施

开学了&#xff0c;宝贝们步入小学的大门&#xff0c;新环境、新同学、新起点。如何为孩子们营造一个和谐、有序的学习环境&#xff1f;这离不开我们班主任的精心管理。 制定明确的班规 教室里&#xff0c;同学们有秩序地坐着&#xff0c;这得益于我们班的班规。但班规不是摆设…

Java SPI 机制介绍和实战

目录 什么是 Java SPI Java SPI 原理 Java SPI 使用场景 1. 框架扩展与插件化 2. 服务加载与扩展 3. 组件化和模块化设计 4. 数据转换和格式化 5. 插件化应用程序 Java SPI 实战 步骤 1&#xff1a;定义服务接口 步骤 2&#xff1a;实现服务提供者 步骤 3&#xf…

UE5 C++(九)— 静态、动态加载类和资源

文章目录 前提静态加载类和资源静态加载资源静态加载类 动态加载类和资源动态资源动态加载类 前提 有必要说一下&#xff0c;静态这块内容加载时我用UE5.2版本出现调用静态资源不是显示问题&#xff0c;修改后容易崩。所以&#xff0c;这里不建议5.2版本&#xff0c;直接用5.3…

VS2005环境下编译C++报错

WinGenerateKey.obj : error LNK2011: 未链接预编译对象&#xff1b;映像可能不能运行 解决&#xff1a;连接器->输入&#xff0c;添加&#xff1a;..\WinGenerateKey\Debug\stdafx.obj 或者 ..\WinGenerateKey\Release\stdafx.obj 报错&#xff1a;fatal error C1083: Can…

【数据结构和算法】找到最高海拔

其他系列文章导航 Java基础合集数据结构与算法合集 设计模式合集 多线程合集 分布式合集 ES合集 文章目录 其他系列文章导航 文章目录 前言 一、题目描述 二、题解 2.1 前缀和的解题模板 2.1.1 最长递增子序列长度 2.1.2 寻找数组中第 k 大的元素 2.1.3 最长公共子序列…

【WSL2】安装和配置ubuntu

文章目录 1. 安装WSL22. 安装ubuntu2.1. 通过Microsoft Store2.1. 通过命令行 3. ubuntu的使用3.1. 创建管理员root账户3.2. 换源3.3. 安装图形化界面GNOME 1. 安装WSL2 在控制面板 - 程序 - 程序与功能中点击启用或关闭Windows功能&#xff0c;选择 虚拟机平台适用于Linux的W…

阅读2023:让每一天都徜徉于书海之中

阅读&#xff0c;是中华民族的优良传统&#xff0c;也是创新发展的永续动力。2023年初&#xff0c;教育部、中央宣传部等八部门印发《全国青少年学生读书行动实施方案》&#xff0c;推动青少年学生阅读深入开展&#xff0c;促进全面提升育人水平。 阅读不仅是文化传承的重要手…

AI智能五子棋这个逆袭.高.智.商.人群的神器竟然是它果断入手

「当当狸智能五子棋」新品现已震撼上市发售啦&#xff0c;将迎来一个全新的对弈时代 市面上首款将智能语音交互&#xff0c;AI陪玩功能融合在棋盘里&#xff0c;且不用摆子收纳的新式棋盘五子棋。 一款打破传统&#xff0c;全面革新&#xff0c;融合AI智能陪玩模式的五子棋诞…

JavaScript基础知识点总结:从零开始学习JavaScript(二)

如果大家感感兴趣也可以去看&#xff1a; &#x1f389;博客主页&#xff1a;阿猫的故乡 &#x1f389;系列专栏&#xff1a;JavaScript专题栏 &#x1f389;ajax专栏&#xff1a;ajax知识点 &#x1f389;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收藏✍️留言 学习…

Vue使用Element table表格格式化GMT时间为Shanghai时间

Vue使用Element表格格式化GMT时间为Shanghai时间 说明 阿里巴巴java开发规范规定&#xff0c;数据库必备gmt_create、gmt_modified字段&#xff0c;使用的是GMT时间&#xff0c;在中国使用必然要转换我中国时间。 在阿里巴巴的Java开发规范中&#xff0c;要求每个表都必备三…

英特尔A770显卡介绍与解读

基础介绍 英特尔A770显卡。这是英特尔推出的一款高性能显卡&#xff0c;属于他们的Arc系列。这个系列的显卡主要面向游戏玩家和专业内容创作者&#xff0c;提供高性能图形处理能力。 A770显卡配备了先进的特性&#xff0c;例如支持硬件级光线追踪、AI加速技术&#xff0c;以及…

【小白专用】C# 压缩文件 ICSharpCode.SharpZipLib.dll效果:

插件描述&#xff1a; ICSharpCode.SharpZipLib.dll 是一个完全由c#编写的Zip, GZip、Tar 、 BZip2 类库,可以方便地支持这几种格式的压缩解压缩, SharpZipLib 的许可是经过修改的GPL&#xff0c;底线是允许用在不开源商业软件中&#xff0c;意思就是免费使用。具体可访问ICSha…

【数据结构】——期末复习题题库(2)

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

基于Java SSM框架实现宜百丰超市进销存购物商城系统项目【项目源码+论文说明】计算机毕业设计

基于java的SSM框架实现宜百丰超市进销存购物商城系统演示 摘要 21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被…

【温故而知新】HTML回流和重绘

概念 HTML回流和重绘是浏览器渲染页面时的两个重要过程。 回流&#xff08;reflow&#xff09;指的是浏览器在渲染页面时&#xff0c;根据页面的结构和样式计算元素的布局和位置。当页面布局或元素位置发生变化时&#xff0c;浏览器需要重新计算元素的布局&#xff0c;这个过…

编写fastapi接口服务

FastAPI是一个基于 Python 的后端框架&#xff0c;该框架鼓励使用 Pydantic 和 OpenAPI (以前称为 Swagger) 进行文档编制&#xff0c;使用 Docker 进行快速开发和部署以及基于 Starlette 框架进行的简单测试。 step1&#xff1a;安装必要库 pip install fastapi uvicorn st…

解决Pycharm pip安装模块太慢问题,pycharm2022没有manage repositories配置镜像源

解决方案 方法清华阿里云中国科技大学华中理工大学 方法 URL写下面任意一个 清华 https://pypi.tuna.tsinghua.edu.cn/simple阿里云 http://mirrors.aliyun.com/pypi/simple/中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple/华中理工大学 http://pypi.hustunique.c…