Kirito 的博客崩了,这次是因为...

Hello 大家好呀,最近 Kirito 的博客因为没有备案的原因,给禁止访问了,折腾了一天才恢复,借着这个机会跟大家闲聊一下个人博客、域名那点事。

从 CSDN 到个人博客

很多人有写作的习惯,只不过有人喜欢写在 QQ 空间,有人喜欢写在专门的博客论坛,也有人喜欢写在个人博客。最开始,我也是在 CSDN 上进行写作的

CSDN旧址

在 2017 年的时候,喜欢折腾的我,开始在个人博客 www.cnkirito.moe 上更新文章。

没有什么特别的原因,只是觉得,这很酷。

Kirito的个人博客

我一直觉得个人博客就像程序员的名片一样,界面的风格和文章的内容都能彰显出一个程序的内心世界。

域名的由来

有些小伙伴很好奇,为什么我的域名:www.cnkirito.moe 是 .moe 结尾的,相比常见的 .com,.moe 的确名不见经传。moe 是日文“萌え”的罗马字写法,这也是汉语中“萌”一词的正确英文写法,当时喜欢二次元的我,购买了 moe 这个顶级域名。至于 cnkirito,懂得都懂,也是二次元的梗。

网站域名解析被封的过程

但申请这个域名时,万万没有想到,5 年后的今天,着实被它坑了一把。由于 moe 顶级域名的提供商是国外的域名提供商,这就给备案带来了很大的问题,按照信安部门的要求,域名解析到国内(香港除外)的服务器,都需要备案,否则就会被封禁。

云服务器通知

我的服务器是之前打比赛的时候,华为云送的 3 年的 4c8g 的机器,我用来搭建一个个人博客那不是绰绰有余吗。结果谁曾想,收到了华为云的通知,让我进行备案。

备案指的是域名解析到某个服务器的过程,并不仅针对域名或者某一服务器。

但我也不是不想备案呀,我的域名是从国外域名商买来的,压根没有备案这个功能。所以摆在我面前的,只有两条路。

  1. 域名转入。缴纳一笔钱,从国外域名商转入到国内域名商,例如阿里云、腾讯云都有这样的服务。
  2. 买一台香港的服务器。重新搭建一个博客。

我自然是不想浪费我的服务器,毕竟这年头买个服务器也不便宜。先尝试走方案一,一般转入域名花费在 100~300 不等,这个费用完全可以接受,毕竟是一次性的,但一番调研之后,却发现此路不通,没有任何一个国内的域名商支持转入 moe 类型的域名!好吧,只怪当年年少,选择了这个小众的域名。

image-20211211214720392

无奈之下,只能选择购买香港的服务器了,可惜了我 4c8g 的云服务器,只能当做实验机了。

搭建博客的建议

有了我上面的教训,如果大家有搭建个人博客的计划,建议当然是购买国内的服务器,使用国内的域名提供商了。

关于服务器的配置,如果是静态博客,像我使用的就是 hexo 搭建的,1c1g 固定带宽 1M 的服务器也完全够用,建议在大促的时候购买或者使用新人/学生等优惠条件,对比下各个云厂商的价格,300400 完全够撑一年,优惠粒度较大时,几十块都能搞定。域名的费用则是 100200 不等。

SEO 优化。路径需要足够短,正例:https://www.cnkirito.moe/learn-mmap,反例:https://www.cnkirito.moe/2021/11/26/learn-mmap,前者有利于搜索引擎的排名。

配置 HTTPS。有不少个人博客图省事,没有搞 HTTPS,我习惯会配置上 HTTPS。一方面小绿锁会提升个人博客的整体格调,另一方面,HTTPS 也会提高搜索引擎的排名(据说。配置也不麻烦,我就是用的七牛云免费的 SSL 证书,再加上 nginx 的 2 行配置而已,并不麻烦。

图床托管。我的图片都是存储在七牛云上,七牛云有一定的免费额度,我的博客只有在某几个人搜索量比较大,超过了额度,付了一定的费用,大多数时候,图片托管基本不会产生费用。

好,以上就是全部的内容啦,Kirito 的博客和公众号会保证同步更新。

PC 端建议访问博客,获得更好的阅读体验:https://www.cnkirito.moe

移动端建议访问微信公众号:Kirito的技术分享 (除了有恰饭广告之外,和博客没有区别 Orz


重新认识 Java 中的内存映射(mmap)

mmap 基础概念

mmap 是一种内存映射文件的方法,即将一个文件映射到进程的地址空间,实现文件磁盘地址和一段进程虚拟地址的映射。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页到对应的文件磁盘上,即完成了对文件的操作而不必再调用 read,write 等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

mmap工作原理

操作系统提供了这么一系列 mmap 的配套函数

1
2
3
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap( void * addr, size_t len);
int msync( void *addr, size_t len, int flags);

【参赛总结】第二届云原生编程挑战赛-冷热读写场景的RocketMQ存储系统设计

前言

人总是这样,年少时,怨恨自己年少,年迈时,怨恨自己年迈,就连参加一场比赛,都会纠结,工作太忙怎么办,周末休息怎么办,成年人的任性往往就在那一瞬间,我只是单纯地想经历一场酣畅的性能挑战赛。所以,云原生挑战赛,我来了,Kirito 带着他的公众号来了。

读完寥寥数百多字的赛题描述,四分之一炷香之后一个灵感出现在脑海中,本以为这个灵感是开篇,没想到却是终章。临近结束,测试出了缓存命中率更高的方案,但评测已经没有了日志,在茫茫的方案之中,我错过了最大的那一颗麦穗,但在一个月不长不短的竞赛中,我挑选到了一颗不错的麦穗,从此只有眼前路,没有身后身,最终侥幸跑出了内部赛第一的成绩。

传统存储引擎类型的比赛,主要是围绕着两种存储介质:SSD 和 DRAM,不知道这俩有没有熬过七年之痒,Intel 就已经引入了第三类存储介质:AEP(PMem 的一种实现)。AEP 的出现,让原本各司其职的 SSD 和 DRAM 关系变得若即若离起来,它既可以当做 DRAM 用,也可以当做 SSD 用。蕴含在赛题中的”冷热存储“这一关键词,为后续风起云涌的赛程埋下了伏笔,同时给了 AEP 一个名分。

AEP 这种存储介质不是第一次出现在我眼前,在 ADB 比赛中就遇到过它,此次比赛开始时,脑子里面对它仅存的印象便是”快”。这个快是以 SSD 为参照物,无论是读还是写,都高出传统 SSD 1~n 个数量级。但更多的认知,只能用 SSD 来类比,AEP 特性的理解和使用方法,无疑是这次的决胜点之一。

曾经的我喜欢问,现在的我喜欢试。一副键盘,一个深夜,我窥探到了 AEP 的奥秘,多线程读写必不可少,读取速度和写入速度近似 DRAM,但细究之下写比读慢,从整体吞吐来看,DRAM 的读写性能略优于 AEP,但 DRAM 和 AEP 的读写都比 SSD 快得多的多。我的麦穗也有了初步的模样:第一优先级是降低 SSD 命中率,在此基础上,提高 DRAM 命中率,AEP 起到平衡的效果,初期不用特别顾忌 AEP 和 DRAM 的命中比例。


Unsafe与ByteBuffer那些事

上一篇文章《聊聊Unsafe的一些使用技巧》写作之后,阅读量很快超过了 1500,Kirito 在这里感谢大家的阅读啦,所以我又来更新了。如果你还没有阅读上一篇文章,我建议你先去看下,闲话不多说,开始今天的话题。

无论是日常开发还是竞赛,Unsafe 不常有而 ByteBuffer 常有,只介绍 Unsafe,让我的博文显得很“炫技”,为了证明“Kirito的技术分享”它可是一个正经的公众号,所以这篇文章会说到另一个比较贴地气的主角 ByteBuffer。我会把我这么多年打比赛的经验传授给你,只求你的一个三连。

从 DirectBuffer 的构造器说起

书接上文,我提到过 DirectBuffer 开辟的堆外内存其实就是通过 Unsafe 分配的,但没有详细介绍,今天就给他补上。看一眼 DirectBuffer 的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DirectByteBuffer(int cap) {                   // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}

短短的十几行代码,蕴含了非常大的信息量,先说关键点

  • long base = unsafe.allocateMemory(size); 调用 Unsafe 分配内存,返回内存首地址
  • unsafe.setMemory(base, size, (byte) 0); 初始化内存为 0。这一行我们放在下节做重点介绍。
  • Cleaner.create(this, new Deallocator(base, size, cap)); 设置堆外内存的回收器,不详细介绍了,可以参考我之前的文章《一文探讨堆外内存的监控与回收》。

仅构造器中的这一幕,便让 Unsafe 和 ByteBuffer 产生了千丝万缕的关联,发挥想象力的话,可以把 ByteBuffer 看做是 Unsafe 一系列内存操作 API 的 safe 版本。而安全一定有代价,在编程领域,一般都有一个常识,越是接近底层的事物,控制力越强,性能越好;越接近用户的事物,更易操作,但性能会差强人意。ByteBuffer 封装的 limit/position/capacity 等概念,用熟悉了之后我觉得比 Netty 后封装 的 ByteBuf 还要简便,但即使优秀如它,仍然有被人嫌弃的一面:大量的边界检查。

一个最吸引性能挑战赛选手去使用 Unsafe 操作内存,而不是 ByteBuffer 地方,便是边界检查。如示例代码一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public ByteBuffer put(byte[] src, int offset, int length) {
if (((long)length << 0) > Bits.JNI_COPY_FROM_ARRAY_THRESHOLD) {
checkBounds(offset, length, src.length);
int pos = position();
int lim = limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
if (length > rem)
throw new BufferOverflowException();
Bits.copyFromArray(src, arrayBaseOffset,
(long)offset << 0,
ix(pos),
(long)length << 0);
position(pos + length);
} else {
super.put(src, offset, length);
}
return this;
}

你不用关心上述这段代码在 DirectBuffer 中充当着什么作用,我想展示给你的仅仅是它的 checkBounds 和 一堆 if/else,尤其是追求极致性能的场景,极客们看到 if/else 会神经敏感地意识到分支预测的性能下降,第二意识是这坨代码能不能去掉。

如果你不希望有一堆边界检查,完全可以借助 Unsafe 实现一个自定义的 ByteBuffer,就像下面这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class UnsafeByteBuffer {

private final long address;
private final int capacity;
private int position;
private int limit;

public UnsafeByteBuffer(int capacity) {
this.capacity = capacity;
this.address = Util.unsafe.allocateMemory(capacity);
this.position = 0;
this.limit = capacity;
}

public int remaining() {
return limit - position;
}

public void put(ByteBuffer heapBuffer) {
int remaining = heapBuffer.remaining();
Util.unsafe.copyMemory(heapBuffer.array(), 16, null, address + position, remaining);
position += remaining;
}

public void put(byte b) {
Util.unsafe.putByte(address + position, b);
position++;
}

public void putInt(int i) {
Util.unsafe.putInt(address + position, i);
position += 4;
}

public byte get() {
byte b = Util.unsafe.getByte(address + position);
position++;
return b;
}

public int getInt() {
int i = Util.unsafe.getInt(address + position);
position += 4;
return i;
}

public int position() {
return position;
}

public void position(int position) {
this.position = position;
}

public void limit(int limit) {
this.limit = limit;
}

public void flip() {
limit = position;
position = 0;
}

public void clear() {
position = 0;
limit = capacity;
}

}

在一些比赛中,为了避免选手进入无止境的内卷,Unsafe 通常是禁用的,但是也有一些比赛,允许使用 Unsafe 的一部分能力,让选手们放飞自我,探索可能性。例如 Unsafe#allocateMemory 是不会受到 -XX:MaxDirectMemory-Xms 限制的,在这次第二届云原生编程挑战赛遭到了禁用,但 Unsafe#putUnsafe#getUnsafe#copyMemory 允许被使用。 如果你一定希望使用 Unsafe 操作堆外内存,可以写出这样的代码,它跟示例代码一完成的是同样的操作。

1
2
3
4
5
byte[] src = ...;

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(src.length);
long address = ((DirectBuffer)byteBuffer).address();
Util.unsafe.copyMemory(src, 16, null, address, src.length);

这便是我想介绍的第一个关键点:DirectByteBuffer 可以借助 Unsafe 完成内存级别细粒度的操作,从而绕开边界检查

DirectByteBuffer 的内存初始化

注意到 DirectByteBuffer 构造器中有另一个涉及到 Unsafe 的操作: unsafe.setMemory(base, size, (byte) 0);。这段代码主要是为了给内存初始化 0。说实话,我是没有太懂这里的初始化操作,因为按照我的认知,默认值也是 0。在某些场景或者硬件下,内存操作是非常昂贵的,尤其是大片的内存被开辟时,这段代码可能会成为 DirectByteBuffer 的瓶颈。

如果希望分配内存时,不进行这段初始化逻辑,可以借助于 Unsafe 分配内存,再对 DirectByteBuffer 进行魔改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class AllocateDemo {

private Field addressField;
private Field capacityField;

public AllocateDemo() throws NoSuchFieldException {
Field capacityField = Buffer.class.getDeclaredField("capacity");
capacityField.setAccessible(true);
Field addressField = Buffer.class.getDeclaredField("address");
addressField.setAccessible(true);
}

public ByteBuffer allocateDirect(int cap) throws IllegalAccessException {
long address = Util.unsafe.allocateMemory(cap);

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1);
Util.unsafe.freeMemory(((DirectBuffer) byteBuffer).address());

addressField.setLong(byteBuffer, address);
capacityField.setInt(byteBuffer, cap);

byteBuffer.clear();
return byteBuffer;
}

}

经过这么一顿操作,我们便得到了一份没有初始化的 DirectByteBuffer,不过不用担心,一切都在正常工作,并且 setMemory for free!

聊聊 ByteBuffer 的零拷贝

算作是题外话了,主要是跟 ByteBuffer 相关的一个话题:零拷贝。 ByteBuffer 在作为读缓冲区时被使用时,有一部分小伙伴会选择使用加锁的方式访问内存,但其实这是非常错误的做法,应当使用 ByteBuffer 提供的 duplicate 和 slice 这两个方法。

并发读取缓冲的方案:

1
2
3
4
5
6
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
ByteBuffer duplicate = byteBuffer.duplicate();
duplicate.limit(512);
duplicate.position(256);
ByteBuffer slice = duplicate.slice();
// use slice

这样便可以在不改变原始 ByteBuffer 指针的前提下,任意对 slice 后的 ByteBuffer 进行并发读取了。

总结

最近时间有限,白天工作,晚上还要抽时间打比赛,先分享这么多。更多性能优化小技巧,可以期待一下 1~2 个星期云原生比赛结束,我就开始继续发总结和其他调优方案。

本文阅读求个 1000,不过分吧!

一键三连,这次一定。


聊聊Unsafe的一些使用技巧

前言

记得初学 Java 那会,刚学完语法基础,就接触到了反射这个 Java 提供的特性,尽管在现在看来,这是非常基础的知识点,但那时候无疑是兴奋的,瞬间觉得自己脱离了“Java 初学者”的队伍。随着工作经验的积累,我也逐渐学习到了很多类似的让我为之而兴奋的知识点,Unsafe 的使用技巧无疑便是其中一个。

sun.misc.Unsafe 是 JDK 原生提供的一个工具类,包含了很多在 Java 语言看来很 cool 的操作,例如内存分配与回收、CAS 操作、类实例化、内存屏障等。正如其命名一样,由于其可以直接操作内存,执行底层系统调用,其提供的操作也是比较危险的。Unsafe 在扩展 Java 语言表达能力、便于在更高层(Java层)代码里实现原本要在更低层(C层)实现的核心库功能上起到了很大的作用。

从 JDK9 开始,Java 模块化设计的限制,使得非标准库的模块都无法访问到 sun.misc.Unsafe。但在 JDK8 中,我们仍然可以直接操作 Unsafe,再不学习,后面可能就没机会了。


使用堆内内存HeapByteBuffer的注意事项

前言

国庆假期一眨眼就过去了,本来在家躺平的很舒服,没怎么肝云原生编程挑战赛,传送门:https://tianchi.aliyun.com/s/8bf1fe4ae2aea736e692c31c6952042d ,偏偏对手们假期开始卷起来了,眼看就要被人反超了,吓得我赶紧继续优化了。比赛大概还有一个月才结束,Kirito 的详细方案也会在比赛结束后分享,这期间我会分享一些比赛中的一些通用优化或者细节知识点,例如本文就是这么一个例子。

趁着假期最后一天,分享一个很多人容易踩得一个坑:HeapByteBuffer 的使用问题。我们都知道 NIO 分装了 ByteBuffer 接口,使得 filechannel 的文件 IO API 变得非常的简单。ByteBuffer 主要有两个实现类

  • HeapByteBuffer 堆内内存
  • DirectByteBuffer 堆外内存

按我的个人经验,大多数情况,无论是读操作还是写操作,我都倾向于使用 DirectByteBuffer,主要是因为 HeapByteBuffer 在和 FileChannel 交互时,可能会有一些出乎大家意料的内部操作,也就是这篇文章的标题中提到的注意事项,这里先卖个关子。

先来看看这次比赛为什么要用到 HeapByteBuffer 呢?

原因一:赛题需要设计分级存储,并且提供了 6G 堆内内存 + 2G 堆外内存,一个最直接的思路便是使用内存来存储热点数据,而内存存储数据最方便的数据结构便是 ByteBuffer 了。

原因二:由于堆内 6G 远大于堆外 2G,且 JVM 参数不能调整,所以要想利用好堆内富余的内存去做缓存,非 HeapByteBuffer 莫属了。

可能有一些读者并没有关注赛题,我这里简化一下前言,可以直接理解为:有一块 2G 的 HeapByteBuffer 用于文件 IO,我们该如何利用。


文件 IO 中如何保证掉电不丢失数据

前言

好久没有分享文件 IO 的小技巧了,依稀记得上次分享还是在上次。

第二届云原生编程挑战赛正在火热进行中,Kirito 也在做《针对冷热读写场景的RocketMQ存储系统设计》这个题目,不过参与的是内部赛道,没法跟外部的小伙伴们一起排名了。

众所周知,存储设计离不开文件 IO,将数据存储到文件中进行持久化,是大多数消息队列、数据库系统的常规操作。在比赛中,为了更贴近实际的生产场景,往往也会引入正确性检测阶段,以避免让选手设计一些仅仅支持内存行为的代码逻辑。试想一下,RocketMQ 或者 Mysql 在宕机之后因为索引丢失,而导致数据无法查询,这该是多么可怕的一件事!

正确性检测要求我们写入的数据能够被查询出来,没有丢失,按照我个人的参赛经验,通常分为三种级别

  • 进程正常退出或者进程被 kill -15 中断
  • 进程被 kill -9 中断
  • 系统掉电

Dubbo中的连接控制,你真的理解吗?

前言

这是一篇很久之前就想动笔写的文章,最近正好看到群里有小伙伴分享了 Dubbo 连接相关的文章,才又让我想起了这个话题。今天想跟大家聊的便是 Dubbo 中的连接控制这一话题。说到“连接控制”,可能有读者还没反应过来,但你对下面的配置可能不会感到陌生:

1
<dubbo:reference interface="com.foo.BarService" connections="10" />

如果你还不了解 Dubbo 中连接控制的用法,可以参考官方文档:https://dubbo.apache.org/zh/docs/advanced/config-connections/ ,话说最近 Dubbo 官方文档来了一次大换血,好多熟悉的文档差点都没找到在哪儿 Orz。

众所周知,dubbo 协议通信默认是长连接,连接配置功能用于决定消费者与提供者建立的长连接数。但官方文档只给出了该功能的使用方法,却并没有说明什么时候应该配置连接控制,本文将主要围绕该话题进行探讨。

本文也会涉及长连接相关的一些知识点。


“字节序”是个什么鬼

转载自:https://zhuanlan.zhihu.com/p/21388517

论顺序的重要性

做饭的故事

今天女朋友加班,机智的她早已在昨晚准备好食材,回家只需下锅便可。谁知开会就是个无底洞,到了B1,还有B2,无穷匮也。

辛苦如她,为了能让她一回家就吃上热腾腾的饭菜,我准备亲自下厨,奉献出我的第一次。食材都已备好,我相信没有那么难,估摸着应该和我习以为常的流程处理差不多,开火 | 加食材 | 上配料 | 翻炒 | 出锅,啊哈,想想还有点小激动。

今天的晚饭是西红柿炒鸡蛋和胡萝卜炒肉,实际操作才发现,又遇到了一个大坑……

食材是这样的:

1
2
案板1号(西红柿炒鸡蛋的食材),从左向右依次放着:西红柿、鸡蛋、葱
案板2号(胡萝卜炒肉的食材),从左向右依次放着:蒜、胡萝卜丝、肉

食材在案板上整齐划一依次排开,我是先放西红柿呢,还是先放鸡蛋呢,还是先放葱呢?简单沟通后得知,案板上的食材是按顺序放好的,我只需要按顺序下锅即可。听着电视哼着90年代的老歌,三下五除二,两道菜如期完成。

闻着怪味,我知道第一次就这么失败了。

等她回家,一番检讨后,才知道是顺序放错了。每道菜都应该是从右往左依次放食材,即葱->鸡蛋->西红柿。这是逗我的么!?一般人所理解的按默认顺序不应该是从左往右嘛!

朋友们,到底应该是从左往右还是从右往左?

剥鸡蛋的故事

《格列佛游记》中记载了两个征战的强国,你不会想到的是,他们打仗竟然和剥鸡蛋的姿势有关。

很多人认为,剥鸡蛋时应该打破鸡蛋较大的一端,这群人被称作“大端(Big endian)派”。可是当今皇帝的祖父小时候吃鸡蛋的时候碰巧将一个手指弄破了。所以,他的父亲(当时的皇帝)就下令剥鸡蛋必须打破鸡蛋较小的一端,违令者重罚,由此产生了“小端(Little endian)派”。

大端序和小端序

老百姓们对这项命令极其反感,由此引发了6次叛乱,其中一个皇帝送了命,另一个丢了王位。据估计,先后几次有11000人情愿受死也不肯去打破鸡蛋较小的一端!

看到没有,仅仅是剥鸡蛋就能产生这么大的分歧,“大端”和“小端”有这么重要嘛!

字节序

字节

字节(Byte)作为计算机世界的计量单位,和大家手中的人民币多少多少“元”一个意思。反正,到了计算机的世界,说字节就对了,使用人家的基本计量单位,这是入乡随俗。

比如,一个电影是1G个字节(1GB),一首歌是10M个字节(10MB),一张图片是1K个字节(1KB)。

字节序

一元钱可以干嘛?啥也干不了,公交都不够坐的。一个字节可以干嘛?至少可以存一个字符。

当数据太大,一个字节存不下的时候,我们就得使用多个字节了。比如,我有两个分别需要4个字节存储的整数,为了方便说明,使用16进制表示这两个数,即0x12345678和0x11223344。有的人采用以下方式存储这个两个数字:

这个方案看起来不错,但是,又有人采用了以下方式:

蒙圈了吧,到底该用哪一种方式来存!两种方案虽有不同,但也有共识,即依次存储每一个数字,即先存0x12345678,再存0x11223344。大家的分歧在于,对于某一个要表示的值,因为只能一个字节一个字节的存嘛,我是把值的低位存到低地址,还是把值的高位存到低地址。前者使用的是“小端(Little endian)”字节序,即先存低位的那一端(两个数字的最低位分别是0x78、0x44),如上图中的第一个图;后者使用的是“大端(Big endian)”字节序,即先存高位的那一端(两个数字的最高位分别是0x12、0x11),如上图中的第二个图。

由此也引发了计算机界的大端与小端之争,不同的CPU厂商并没有达成一致:

  • x86,MOS Technology 6502,Z80,VAX,PDP-11等处理器为Little endian。
  • Motorola 6800,Motorola 68000,PowerPC 970,System/370,SPARC(除V9外)等处理器为Big endian。
  • ARM, PowerPC (除PowerPC 970外), DEC Alpha, SPARC V9, MIPS, PA-RISC and IA64的字节序是可配置的。

大端也好,小端也罢,就权当是个人爱好吧,只要你不影响别人就行,对不?

网络字节序

前面的大端和小端都是在说计算机自己,也被称作主机字节序。其实,只要自己能够自圆其说是没啥问题的。问题是,网络的出现使得计算机可以通信了。通信,就意味着相处,相处必须得有共同语言啊,得说普通话,要不然就容易会错意,下了一个小时的小电影发现打不开,理解错误了!

但是每个计算机都有自己的主机字节序啊,还都不依不饶,坚持做自己,怎么办?

TCP/IP协议隆重出场,RFC1700规定使用“大端”字节序为网络字节序,其他不使用大端的计算机要注意了,发送数据的时候必须要将自己的主机字节序转换为网络字节序(即“大端”字节序),接收到的数据再转换为自己的主机字节序。这样就与CPU、操作系统无关了,实现了网络通信的标准化。突然觉得,TCP/IP协议好任性啊有木有!

为了程序的兼容,你会看到,程序员们每次发送和接受数据都要进行转换,这样做的目的是保证代码在任何计算机上执行时都能达到预期的效果。

这么常用的操作,BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。当然,有了上面的理论基础,也可以编写自己的转换函数。

下面的一段代码可以用来判断计算机是大端的还是小端的,判断的思路是确定一个多字节的值(下面使用的是4字节的整数),将其写入内存(即赋值给一个变量),然后用指针取其首地址所对应的字节(即低地址的一个字节),判断该字节存放的是高位还是低位,高位说明是Big endian,低位说明是Little endian。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main ()
{
unsigned int x = 0x12345678;
char *c = (char*)&x;
if (*c == 0x78) {
printf("Little endian");
} else {
printf("Big endian");
}
return 0;
}

身边的字节序

字符编码方式UTF-16、UTF-32同样面临字节序的问题,因为他们分别使用2个字节和4个字节编码Unicode字符,一旦某个值用多个字节表示,就必须要考虑存储的顺序了。于是,采用了最简单粗暴的方式,给文件头部写几个字符,用来表示是大端呢还是小端:

头部的字符 编码 字节序 FF FE UTF-16/UCS-2 Little endian FE FF UTF-16/UCS-2 Big endian FF FE 00 00 UTF-32/UCS-4 Little endian 00 00 FE FF UTF-32/UCS-4 Big-endian

这里不得不提一下UTF-8啊,明明人家是单个字节的,不存在什么字节序的问题。微软为了统一UTF-X,硬生生给他的头部也加了几个字符!是的,这几个字符就是BOM(Byte Order Mark),这就是Windows下的UTF-8。

相信很多人都被UTF-8的BOM给坑过,多了这个BOM的UTF-8文件,会导致很多问题啊。比如,写的Shell脚本,内容为#!/usr/bin/env bash,在UTF-8有BOM和UTF-8无BOM的编码下,对应的16进制为:

BOM

所以,有BOM的话,Shell解释器就报错啦。原因在于,解释器希望遇到#!/usr/bin/env bash,而使用UTF-8有BOM进行编码的内容会多了3个字节的EF BB BF。

对于UTF-8和UTF-8无BOM两种编码格式,我们更多的使用UTF-8无BOM。


如何更快地将string转换成int/long

在很多追求性能的程序挑战赛中,经常会遇到一个操作:将 String 转换成 Integer/Long。如果你没有开发过高并发的系统,或者没有参加过任何性能挑战赛,可能会有这样的疑问:这有啥好讲究的,Integer.valueOf/Long.valueOf 又不是不能用。实际上,很多内置的转换工具类只满足了功能性的需求,在高并发场景下,可能会是热点方法,成为系统性能的瓶颈。

文章开头,我先做一下说明,本文的测试结论出自:https://kholdstare.github.io/technical/2020/05/26/faster-integer-parsing.html 。测试代码基于 C++,我会在翻译原文的同时,添加了部分自己的理解,以协助读者更好地理解其中的细节。


Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×