您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

指针与内存对齐到底是什么鬼?

时间:2023-04-15 14:49:39  来源:今日头条  作者:技术小兵

什么是指针?先看看什么是内存地址

首先,我们要搞清楚数据结构在计算机里面到底怎么存取?怎么描述它们。

任何数据结构(struct)以及组成数据结构的基本数据类型,一旦分配了内存空间,那么就会有两个部分来描述这块内存:内存的地址(红色部分,不占用实际空间,相当于门牌号,用于寻址)与内存的值(绿色部分,是实际的信息存储部分,占用内存空间,以byte为单位)。就像下面这张图:

数据在内存中的结构

所以一块内存,或者一个符号编程语言的符号其实就代表了一块内存,所以它们代表同一个意思)有两个重要的属性:

 

  1. 内存或者符号的地址
  2. 内存或者符号的值

 

这两个属性如同一个事物的两面,不可分割,形影不离。

有时候,如果对事情的本质进行深挖的话,你可能对一些基本概念有更深刻的理解。比如,到这里,如果你理解了内存或者编程语言的符号有两个基本的属性:地址与值,那么你就可以理解C/C++中的&与=操作符的含义。

 

  • &作用在一个符号上的底层含义就是——获取这个符号的两个重要属性之一——符号的地址
  • =作用在一个符号上的底层含义就是——获取这个符号的两个重要属性之一——符号的内存值。int a=1;含义就是获取符号a的内存值,并将内存值赋值成1。

 

可以推断出,从CPU的角度,或者编程语言底层来看,没有数据类型的概念,任何数据都是一块块连续的、长短不一的内存存储单元而已,就像上图所画。那么问题就变成了,怎么描述这块内存呢?

答案是:内存的起始地址+长度。比如下面这个结构:

struct Test {int a;short b;

对于test这个结构,怎么描述它?

答案是:struct test是——符号a的内存地址+6个字节长度的数据块,如果要读取或者写入test某个部分(a或者b),编译器至少要编译两条指令:1、获取test也就是a符号的地址,2、根据类型定位偏移量就行了。这就是数据结构的本质了。

那么对数据结构成员变量的访问就很容易理解了:

 

  • test.a就可以被编译成符号a的地址+向高地址取4个字节的内存块。
  • test.b就可以看成符号a的地址向高地址偏移4个字节+向高地址取2个字节的内存块。

 

是不是有点类似数学中的极坐标系的概念。而实际上系统确实是这么做的。

站在编译器的角度看看符号与变量

指针在C与C++中很难理解,但是又是重要的构成部分,没有了指针其实就发挥不出语言的光芒了。因为指针是很自然的事物,它是承接CPU取址与程序可读性的关键概念,理解了它就既能看穿机器的运行,又能写出合理的优雅的代码去描述业务。

要真正理解指针或者更普遍的意义来说,理解符号,就得将自己想象成编译器去读代码,这样一切都会变得理所当然的容易起来。

我们看到的程序都是由变量符号组成的,本质上符号代表一块内存,比如上面的结构体就有三个变量符号或者简称符号:test,a,b。每个符号其实都对应一块型如下图的内存块:

内存块的两个维度

再来看看这个代码片段

typedef struct test {int a;short b;} Test;Test t;t.a =1;t.b =2;Test* t_ptr = &t;t_ptr->a = 3;

  • Test t:如果“我”是编译器,看到这行代码,我获得的信息是:t是一个符号,它有两个维度的信息:1、地址是&t;2、长度是sizeof(Test) = 6(不考虑对齐)。而且我会自动补全表达式为Test t = 0初始化t代表的这块内存。生成的底层代码应该做这些:1、给符号t分配一个内存地址,地址一共6个byte长度;2、将这6个byte的地址的值都填充为0。
  • t.a = 1:语义是给符号a的值,赋值1。符号a的地址就是t的地址,符号a的长度4个字节,=的含义就是获取a的内存值,最后将int 1填充到这4个字节的内存中,完成赋值。
  • t.b = 2:语义是给符号b的值,赋值2。符号b的地址是t的地址往高处偏移a的长度;同时符号b的值的长度是2个字节,=就是获取b的内存值,最后将short 2填充到这2个字节的内存中,完成赋值。
  • 再看看复杂点的Test* t_ptr = &t;:t_ptr是个符号,它有地址与值两个属性。Test*修饰部分用来描述t_ptr的长度,这里的Test*说明 t_ptr的值是一块内存的起始地址,长度为8字节(x64平台)。这就是按照编译器的角度解释指针:不管*号前面是什么类型(task_struct* ptr还是int* ptr还是char* ptr)被修饰的符号长度永远就是8个字节的地址。而t_ptr = &t就是将符号t_ptr的值赋值成符号t的地址——就是t_ptr这个符号的地址开始连续8个byte的内存填入符号t的地址值。
  • 看了指针的本质,我们来看看对指针的操作指令:t_ptr->a = 3。=的本质是获取符号a的值进行赋值;而找到a的地址比较复杂,先要拿到t_ptr符号的值,也就是&t,然后在t的地址基础根据a的偏移找到a的地址,这里偏移是0,a的地址等于t的地址。然后根据a的类型int将int 3赋值到这4个字节上就行了。
到这里我们可以推敲一下指针的本质了

 

接着上面的例子,我们已经分析了t_ptr的内存布局,它的值是一个地址。问题就来了,你想过没有,如果一个符号,它的值保存了一个地址,我对他能做什么操作?我们知道,如果t_ptr的值是int、long,我就能用CPU的算术模块对它们进行“加减乘除”,这样是有意义的,因为我在做代数运算。那么对一个地址,显然,做加减乘除运算是没有意义的。我们唯一能对地址做的有意义的操作就是找到这块地址,对这个地址对应的内存进行操作,这才是地址类型数据的意义。

因为对地址进行普通意义上的四则运算是没有代数意义的,所以,C语言为地址数据类型(指针)增加了两个操作符*与->。

 

  • *就是切换符号的含义,如*ptr = 3那么=获取的内存值,并不是ptr这个符号本身的值,而是ptr的值所对应的内存地址的内存值。相当于将符号ptr的含义进行了切换,切换到了新的目标内存地址;
  • ->也是切换符号含义,但是不是切换到ptr指向的大内存块,而是里面的小内存块,可以理解成切换成对成员变量的符号的内存访问。
看看linux中一些指针操作——二重指针

 

看个Linux内核中的例子,这是mcs spinlock的加锁操作

static inlinevoid mcs_spin_lock(struct mcs_spinlock **lock, struct mcs_spinlock *node)struct mcs_spinlock *prev;/* Init node */node->locked = 0;node->next = NULL;prev = xchg(lock, node); //相当于把mcslock.next = node;同时返回*lock修改之前的值。if (likely(prev == NULL)) { //原来*lock指向NULL。也就是现在链表还没形成,没有竞争。return;} // 如果有值说明有竞争,要排队。所以直接插入最后就行了。prev就是最后一个元素。WRITE_ONCE(prev->next, node);/*这里是个spin loop。在percpu自身的lock上面自旋,等待变成1,获取锁。/* WAIt until the lock holder passes the lock down. */arch_mcs_spin_lock_contended(&node->locked);

  • struct mcs_spinlock **lock是什么呢?书上会说指向指针的指针,这么说没错,但是对很多刚刚接触到C语言的人来说其实很难理解,很难对应到实际内存中的样子。不如,一步步拆解这条语句的含义。
    • 首先,我们要先找到符号,符号才是内存,才有意义。显然这里描述的符号是lock;
    • 符号就是一块内存,是内存就有地址与值,那么lock的值的信息可以从它的类型来推断出来,struct mcs_spinlock **就是类型信息,它的主要作用是描述lock的值有多长。那么有多长呢?看到*就不用看struct mcs_spinlock了,就是一个地址,另一个符号的地址,8个字节长,保存在了lock符号的值里面。
    • struct mcs_spinlock **的含义是什么呢?这是一个递归定义。根据前面对*运算符的解释,struct mcs_spinlock **可以展开成这种形式(struct mcs_spinlock *)(*lock)。*lock的含义是切换符号,假设切换成了(struct mcs_spinlock *)_lock符号,_lock符号的地址是lock的值,而_lock的值又是一个地址,一个struct mcs_spinlock *类型的地址。如下图:

 

指针的指针到底是什么?

 

  • 而*lock = node;的含义就是:将符号node的值赋给*lock符号的值。

 

*就表示切换符号

 

  • 所以,不管多少个*都可以递归化简了。
  • 比如:T *** t = &node;这个表达式:

 

图解三重指针

看看Linux中一些指针操作——链表操作//链表头指针struct wake_q_head {struct wake_q_node *first;struct wake_q_node **lastp;struct wake_q_node {struct wake_q_node *next;//初始化链表头static inline void wake_q_init(struct wake_q_head *head)head->first = WAKE_Q_TAIL; // #define WAKE_Q_TAIL ((struct wake_q_node *) 0x01)head->lastp = &head->first;//添加新元素static bool __wake_q_add(struct wake_q_head *head, struct task_struct *task)struct wake_q_node *node = &task->wake_q;* Atomically grab the task, if ->wake_q is !nil already it means* it's already queued (either by us or someone else) and will get the* wakeup due to that.* In order to ensure that a pending wakeup will observe our pending* state, even in the failed case, an explicit smp_mb() must be used.smp_mb__before_atomic();if (unlikely(cmpxchg_relaxed(&node->next, NULL, WAKE_Q_TAIL)))return false;* The head is context local, there can be no concurrency.*head->lastp = node;head->lastp = &node->next;return true;

wake_q_init

添加第一个元素

 

  • first指向task_struct中的成员wake_q;指向队列的第一个元素;
  • lastp指向task_struct的成员wake_q的成员next;next也是一个指针;

 

再添加一个元素:

再添加一个元素

 

  • first始终指向第一个元素;
  • lastp始终指向最后一个元素的next符号。
关于指针你只要记住
  • 看到表达式先找到符号;
  • 符号就等同于内存空间;
  • 符号有地址与值两个维度的属性,脑子中画一个上面的图来帮助理解;
  • *、->这两个操作符的本质就是从一个符号切换到另一个符号,从一块内存切换到另一块;
  • &与=这两个操作符号的本质就是获取内存值。
理解了指针就能更进一步理解内存对齐了

 

内存对齐应该叫做内存的虚拟地址对齐,讲的是如果我们为一个数据结构——抽象来讲就是一块内存——分配一个地址的时候,需要满足的规则。那么规则有哪些?我们可以先列出来:

 

  • 基本数据类型(int,short,char,byte,long,float,double)的变量的首字节地址必须是类型字节数的整数倍;
  • 结构体(首字节地址)必须是最大成员变量数据类型的整数倍(编译器维护);
  • 结构体中每个成员变量的首字节地址,必须是成员类型的整数倍,如果不是,则编译器填充实现;
  • 结构体的总体长度必须是最大成员变量类型长度的整数倍,如果不是,编译器在结构体最后一个字节末尾填充0实现。

 

下面具体说明下这些规则都是什么意思。

基本数据类型的首地址必须是类型字节数的整数倍

还是这个代码片段:

#include#includetypedef struct test {short b;int a;} Test;int main(){printf("Test size is:%ldn",sizeof(Test));Test* t_ptr = (Test*)malloc(sizeof(Test));t_ptr->a = 1;t_ptr->b = 2;printf("a is:%dn",t_ptr->a);printf("b is:%dn",t_ptr->b);

为了迎合这个问题,我们调换了a,b符号在结构体中出现的顺序。之前我们假设sizeof(Test)是6,但是真的如此吗?我们看看运行的结果:

Test size is:8a is:1b is:2

其实是8个字节。为啥呢?就是因为int a要符合基本数据类型的首地址必须是类型字节数的整数倍这条规则,所以编译器会在b与a之间插入2个字节的0,使得a的首字节地址是int的整数倍;变成:

typedef struct test {short b;short invisible_padding; //实际看不见int a;} Test;

反汇编验证下:

t_ptr->a = 1;119c: 48 8b 45 f8 mov -0x8(%rbp),%rax /*拿到符号t_ptr的地址*/11a0: c7 40 04 01 00 00 00 movl $0x1,0x4(%rax) /*执行 = 操作符,给符号a的内存赋值*/t_ptr->b = 2;11a7: 48 8b 45 f8 mov -0x8(%rbp),%rax /*拿到符号t_ptr的地址*/11ab: 66 c7 00 02 00 movw $0x2,(%rax) /*执行 = 操作符,给符号b的内存赋值*/

可以从反汇编看到a的内存地址从偏移地址0x4开始,而b从偏移地址0x2开始,而padding是放在t_ptr的开始位置的,这跟我的猜想有点出入,但是并不破坏规则,因为int a的首字节地址依然变成了4的整数倍。如下图:

反汇编1

那么问题就来了,为什么要填充呢?本质的原因是什么?

从CPU角度看看为什么要对齐

一图胜千言:

CPU角度看内存加载问题

解释:

 

  • 内存的访问真的没有程序员想的那么简单,而是分组读取的,也就是总线32位宽,其实不是连续的,而是分成了4组,每组读取1个字节,然后拼成一个双字的数据块;x64就分成8个组;
  • 可以把组看成一个通道,CPU可以一次激活最多4个(32位)或者8个(64位)通道,一次读取可以看成一个transaction;
  • 每个通道一次读取一个字节的数据;
  • 每个通道读取的地址是有规律的,比如1号通道(0,4,8,12,16…)二号通道(1,5,9,13,…)依次类推;
  • 数据读取性能跟所需的transaction数量相关,越少性能越高;
  • 根据以上的事实,内存对齐的定义其实就是——让数据结构的首字节地址始终在通道1上就是对齐的数据,否则,就不是;
  • 符号首地址是n字节对齐的含义是:**符号首字节地址是n字节的倍数。**比如,下图,int a就是4字节对齐的,第二个int a’是6字节对齐的。
  • 数据不对齐会比对齐的数据,在访问时,多1次内存的开销。

 

一图胜千言,上图:

对齐的与没有对齐的内存读取差别

所以,内存必须对齐,不然同样的数据结构,没对齐比对齐后的内存要多一次内存的开销。

不要小看这一次内存访问的开销,因为:

 

  • CPU可以说每时每刻都在以超高并发量访问内存,假如1秒1千次的内存访问,如果都多一次,一秒就是2千次,性能会下降50%。
  • 根据性能金字塔,内存的访问可是在底层,延迟是很大的,所以在CPU这种高并发的场景下,特别是多核的SMP系统,性能问题就会更加严重。
关于结构体的三条规则
  1. 结构体(首字节地址)必须是最大成员变量数据类型的整数倍(编译器维护);
  2. 结构体中每个成员变量的首字节地址,必须是成员类型的整数倍,如果不是,则编译器填充实现;
  3. 结构体的总体长度必须是最大成员变量类型长度的整数倍,如果不是,编译器在结构体最后一个字节末尾填充0实现。

 

其中2.就是基本数据类型的首地址必须是类型字节数的整数倍的推论,或者说是等价的,不需要证明。

关于1.与3.的证明,需要引入一个推论:如果符号的首地址是n字节对齐的,那么一定是n/2对齐的,也一定是n/4对齐的,依次类推下去。

举个例子来说就是:符号a的首地址如果是8字节对齐,那么一定也是4字节对齐,一定也是2字节对齐的。其实很容易证明:如果a的地址是x,x%8 = 0;那么x = b×8;x%4 = b×4×2 %4=0;所以也是4字节对齐的。

结构体(首字节地址)必须是最大成员变量数据类型的整数倍的证明

可以由2.推导而来。步骤是:

1、假设结构体中的最大成员变量的长度是long8个字节;那么,根据2.可知,这个变量前面的变量的长度总和,必须是8的整数倍;

2、所以,如果结构体的首字节地址不是8的整数倍,那么就算最大成员变量之前的所有成员变量长度和满足了是8的整数倍,也不能保证2.的成立;

3、所以结构体的首字节地址必定是最大成员长度的整数倍,也就是8字节对齐的。

内存布局

结构体的总体长度必须是最大成员变量类型长度的整数倍证明

这个主要是考虑数组的情况。在单个结构体中对齐的数据,必须在数组情况下也是对齐的,如果最大的成员变量是对齐的,则所有其他成员变量都是对齐的。证明如下图:

证明

以一个例子总结#include#includetypedef struct testint a; // 4// padding 4long long b; // 8 b要8字节对齐,也就是前面的字节数要是8的倍数,而前面只有4字节,所以要padding4个字节char c; // 1// padding 1short d; // 2 同样d前面的字节数要是2的倍数,所以padding 1个字节// padding 4 前面整体的test结构只有20个字节,而整体的大小也要是最大元素的整数倍,因为如果是数组,那么两个my的元素那么第二个my的b变量位于28的位置,不是8的整数倍,所以结尾再padding4个字节。凑成24个字节。} My;int main(int argc, char *argv[])//栈上分配My my;my.a = 1;11ab: c7 45 e0 01 00 00 00 movl $0x1,-0x20(%rbp)my.b = 2L;11b2: 48 c7 45 e8 02 00 00 movq $0x2,-0x18(%rbp)11b9: 00my.c = 'a';11ba: c6 45 f0 61 movb $0x61,-0x10(%rbp)my.d = 4;11be: 66 c7 45 f2 04 00 movw $0x4,-0xe(%rbp)My my;my.a = 1;my.b = 2L;my.c = 'a';my.d = 4;printf("size of my:%dn", sizeof(My));printf("address of my is:%xn", &my);//堆上分配11f8: bf 18 00 00 00 mov $0x18,%edi11fd: e8 8e fe ff ff call 10901202: 48 89 45 c0 mov %rax,-0x40(%rbp)my_on_heap->a = 1;1206: 48 8b 45 c0 mov -0x40(%rbp),%rax120a: c7 00 01 00 00 00 movl $0x1,(%rax)my_on_heap->b = 2L;1210: 48 8b 45 c0 mov -0x40(%rbp),%rax1214: 48 c7 40 08 02 00 00 movq $0x2,0x8(%rax)121b: 00my_on_heap->c = 'a';121c: 48 8b 45 c0 mov -0x40(%rbp),%rax1220: c6 40 10 61 movb $0x61,0x10(%rax)my_on_heap->d = 4;1224: 48 8b 45 c0 mov -0x40(%rbp),%rax1228: 66 c7 40 12 04 00 movw $0x4,0x12(%rax)My* my_on_heap = (My*)malloc(sizeof(My));my_on_heap->a = 1;my_on_heap->b = 2L;my_on_heap->c = 'a';my_on_heap->d = 4;printf("address of my is:%xn", my_on_heap);参考

视频——内存对齐到底是个什么鬼



Tags:指针   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
Go函数指针是如何让你的程序变慢的?
导读Go 语言的常规优化手段无需赘述,相信大家也能找到大量的经典教程。但基于 Go 的函数值问题,业界还没有太多深度讨论的内容分享。本文作者根据自己对 Go 代码的使用与调优...【详细内容】
2024-01-15  Search: 指针  点击:(88)  评论:(0)  加入收藏
指针变量在C/C++中的内存占用
在编程领域,尤其是C和C++这类底层语言中,指针是一个核心概念,它允许程序直接操作内存地址。然而,关于指针本身在内存中占用的空间大小,却常常让初学者感到困惑。本文将深入探讨这...【详细内容】
2024-01-09  Search: 指针  点击:(95)  评论:(0)  加入收藏
C++传递大型对象:传值、传引用还是传指针?
一、引言在C++编程中,当我们需要将大型对象作为参数传递给函数时,常常会遇到一个问题:应该使用传值、传引用还是传指针?每种传递方式都有其优缺点,因此需要根据具体情况进行选择...【详细内容】
2023-11-29  Search: 指针  点击:(190)  评论:(0)  加入收藏
C++函数返回指针和引用的坑
常用C++进行项目开发的童鞋们应该都知道,在C++中指针和引用是常用的语法了,而指针又是C++区别于其他高级语言的一大精髓。而今天我们再来看看在C++新手们针对指针和引用的使用...【详细内容】
2023-11-23  Search: 指针  点击:(239)  评论:(0)  加入收藏
Java空指针检查实在看不下去了——转用Optional真香
前言在Java开发中,空指针是程序员遇到的最多的异常之一(特别是刚接触java开发的),对于对象中的某个属性,有时候我们为了避免程序报空指针错误,而不得不使用较多的if、else来进行逻...【详细内容】
2023-11-14  Search: 指针  点击:(270)  评论:(0)  加入收藏
使用Optional优雅避免空指针异常
在编程世界中,「空指针异常(NullPointerException)」无疑是我们最常遇到的"罪魁祸首"之一。它像一片隐蔽的地雷,静静地等待着我们不小心地踏入,给我们的代码带来潜在的威胁。这种...【详细内容】
2023-09-26  Search: 指针  点击:(270)  评论:(0)  加入收藏
重大线上事故!三元表达式引发的空指针问题
属实刺激,刚入职不久就遇到这种史诗级的线上 Bug,首页直接崩溃,陈年老代码爆雷,不管落到最后的底层原因是什么,我感觉主要还是上下游的链路太过复杂,治理难度比较大,牵一发而动全身...【详细内容】
2023-09-07  Search: 指针  点击:(264)  评论:(0)  加入收藏
理解指针和数组:计算机科学的必备基础知识
在计算机科学中,指针和数组是两个非常重要的概念,它们在编程中有着广泛的应用。本文将探讨指针和数组的基本概念、使用方法以及它们在程序中的应用。一、指针指针是计算机程序...【详细内容】
2023-09-05  Search: 指针  点击:(230)  评论:(0)  加入收藏
Go 中的指针:了解内存引用
在 Go 中,指针是强大而重要的功能,它允许开发人员直接处理内存地址并实现高效的数据操作。指针提供了一种传递数据引用而不是复制整个数据的方法,这对大型数据结构尤其有益。了...【详细内容】
2023-08-31  Search: 指针  点击:(169)  评论:(0)  加入收藏
C语言指针数组类面试题大全
在C语言的面试中,指针数组是一个非常重要的知识点。它不仅是C语言中最基本的数据结构之一,也是面试官经常考察的知识点。在本篇博客中,我们将为大家整理一些常见的C语言指针数...【详细内容】
2023-08-06  Search: 指针  点击:(413)  评论:(0)  加入收藏
▌简易百科推荐
Netflix 是如何管理 2.38 亿会员的
作者 | Surabhi Diwan译者 | 明知山策划 | TinaNetflix 高级软件工程师 Surabhi Diwan 在 2023 年旧金山 QCon 大会上发表了题为管理 Netflix 的 2.38 亿会员 的演讲。她在...【详细内容】
2024-04-08    InfoQ  Tags:Netflix   点击:(0)  评论:(0)  加入收藏
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(6)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(13)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(9)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(11)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(9)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
站内最新
站内热门
站内头条