2024年1月

写在前面

在很久很久以前,网页布局基本上通过
table
元素来实现。通过操作
table
中单元格的
align

valign
可以实现水平垂直居中等

再后来,由于
CSS
不断完善,便演变出了:
标准文档流

浮动布局

定位布局
3种布局 来实现水平垂直居中等各种布局需求。

下面我们来看看实现如下效果,各种布局是怎么完成的

image-20240114134424060

实现这样的布局方式很多,为了方便演示效果,我们在html代码种添加一个父元素,一个子元素,css样式种添加一个公共样式来设置盒子大小,背景颜色

<div class="parent">
    <div class="child">我是子元素</div>
</div>
/* css公共样式代码 */
.parent{
    background-color: orange;
    width: 300px;
    height: 300px;
}
.child{
    background-color: lightcoral;
    width: 100px;
    height: 100px;
}

①absolute + 负margin 实现

/* 此处引用上面的公共代码 */

/* 定位代码 */
.parent {
    position: relative;
}
.child {
    position: absolute;;
    top: 50%;
    left: 50%;
    margin-left: -50px;
    margin-top: -50px;
}

②absolute + transform 实现

/* 此处引用上面的公共代码 */

/* 定位代码 */
.parent {
    position: relative;
}
.child {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

③ flex实现

.parent {
    display: flex;
    justify-content: center;
    align-items: center;
}

通过上面三种实现来看,我们应该可以发现
flex
布局是最简单了吧。

对于一个后端开发人员来说,flex布局算是最友好的了,因为它操作简单方便

一、flex 布局简介

flex
全称是
flexible Box
,意为
弹性布局
,用来为盒状模型提供布局,任何容器都可以指定为flex布局。

通过给父盒子添加flex属性即可开启弹性布局,来控制子盒子的位置和排列方式。

父容器可以统一设置子容器的排列方式,子容器也可以单独设置自身的排列方式,如果两者同时设置,以子容器的设置为准

flex布局

二、flex基本概念

flex的核心概念是
容器


,容器包括外层的
父容器
和内层的
子容器
,轴包括
主轴

辅轴

<div class="parent">
    <div class="child">我是子元素</div>
</div>

2.1 轴

  • 在 flex 布局中,是分为主轴和侧轴两个方向,同样的叫法有 : 行和列、x 轴和y 轴,主轴和交叉轴

  • 默认主轴方向就是 x 轴方向,水平向右

  • 默认侧轴方向就是 y 轴方向,水平向下

    主轴和侧轴

注:主轴和侧轴是会变化的,就看
flex-direction
设置谁为主轴,剩下的就是侧轴。而我们的子元素是跟着主轴来排列的

--flex-direction 值 --含义
row 默认值,表示主轴从左到右
row-reverse 表示主轴从右到左
column 表示主轴从上到下
column-reverse 表示主轴从下到上

2.2 容器

容器的属性可以作用于父容器(container)或者子容器(item)上

①父容器(container)-->属性添加在父容器上

  • flex-direction 设置主轴的方向
  • justify-content 设置主轴上的子元素排列方式
  • flex-wrap 设置是否换行
  • align-items 设置侧轴上的子元素排列方式(单行 )
  • align-content 设置侧轴上的子元素的排列方式(多行)

②子容器(item)-->属性添加在子容器上

  • flex 属性 定义子项目分配剩余空间,用flex来表示占多少份数
  • align-self控制子项自己在侧轴上的排列方式
  • order 属性定义项目的排列顺序

三、主轴侧轴设置

3.1 flex-direction: row

flex-direction: row 为默认属性,
主轴沿着水平方向向右,元素从左向右排列。

row

3.2 flex-direction: row-reverse

主轴沿着水平方向向左,子元素从右向左排列

row-reverse

3.3 flex-direction: column

主轴垂直向下,元素从上向下排列

column

3.4 flex-direction: column-reverse

主轴垂直向下,元素从下向上排列

column-reverse

四、父容器常见属性设置

4.1 主轴上子元素排列方式

4.1.1 justify-content

justify-content
属性用于定义主轴上子元素排列方式

justify-content: flex-start|flex-end|center|space-between|space-around


flex-start
:起始端对齐

flex-start


flex-end
:末尾段对齐

flex-end


center
:居中对齐

center


space-around
:子容器沿主轴均匀分布,位于首尾两端的子容器到父容器的距离是子容器间距的一半。

space-around


space-between
:子容器沿主轴均匀分布,位于首尾两端的子容器与父容器相切。

space-between

4.2 侧轴上子元素排列方式

4.2.1 align-items 单行子元素排列

这里我们就以默认的x轴作为主轴


align-items:flex-start
:起始端对齐

flex-start


align-items:flex-end
:末尾段对齐

flex-end


align-items:center
:居中对齐

center


align-items:stretch
侧轴拉伸对齐

如果设置子元素大小后不生效

stretch

4.2.2 align-content 多行子元素排列

设置子项在侧轴上的排列方式 并且只能用于子项出现 换行 的情况(多行),在单行下是没有效果的

我们需要在父容器中添加
flex-wrap: wrap;

flex-wrap: wrap;
是啥意思了,具体会在下一小节中细说,就是当所有子容器的宽度超过父元素时,换行显示


align-content: flex-start 起始端对齐

 /* 父容器添加如下代码 */
display: flex;
align-content: flex-start;
flex-wrap: wrap;

align-content: flex-start


align-content: flex-end :末端对齐

/* 父容器添加如下代码 */
display: flex;
align-content: flex-end;
flex-wrap: wrap;

align-content: flex-end


align-content: center: 中间对齐

/* 父容器添加如下代码 */
display: flex;
align-content: center;
flex-wrap: wrap;

align-content: center


align-content: space-around:
子容器沿侧轴均匀分布,位于首尾两端的子容器到父容器的距离是子容器间距的一半

/* 父容器添加如下代码 */
display: flex;
align-content: space-around;
flex-wrap: wrap;

align-content: space-around


align-content: space-between
:子容器沿侧轴均匀分布,位于首尾两端的子容器与父容器相切。

/* 父容器添加如下代码 */
display: flex;
align-content: space-between;
flex-wrap: wrap;

image-20240114171606954


align-content: stretch
: 子容器高度平分父容器高度

/* 父容器添加如下代码 */
display: flex;
align-content: stretch;
flex-wrap: wrap;

align-content: stretch

4.3 设置是否换行

默认情况下,项目都排在一条线(又称”轴线”)上。flex-wrap属性定义,flex布局中默认是不换行的。


flex-wrap: nowrap
:不换行

/* 父容器添加如下代码 */
display: flex;
flex-wrap: nowrap;

flex-wrap: nowrap


flex-wrap: wrap
: 换行

/* 父容器添加如下代码 */
display: flex;
flex-wrap: wrap;

flex-wrap: wrap

4.4 align-content 和align-items区别

  • align-items 适用于单行情况下, 只有上对齐、下对齐、居中和 拉伸
  • align-content适应于换行(多行)的情况下(单行情况下无效), 可以设置 上对齐、下对齐、居中、拉伸以及平均分配剩余空间等属性值。
  • 总结就是单行找align-items 多行找 align-content

五、子容器常见属性设置

  • flex子项目占的份数
  • align-self控制子项自己在侧轴的排列方式
  • order属性定义子项的排列顺序(前后顺序)

5.1 flex 属性

flex 属性定义子项目分配剩余空间,用flex来表示占多少份数。

① 语法

.item {
    flex: <number>; /* 默认值 0 */
}

②将1号、3号子元素宽度设置成
80px
,其余空间分给2号子元素

flex:1

5.2 align-self 属性

align-self 属性允许单个项目有与其他项目不一样的对齐方式,可覆盖 align-items 属性。

默认值为 auto,表示继承父元素的 align-items 属性,如果没有父元素,则等同于 stretch。


align-self: flex-start 起始端对齐

/* 父容器添加如下代码 */
display: flex;
align-items: center;
/*第一个子元素*/
align-self: flex-start;

align-self: flex-start


align-self: flex-end 末尾段对齐

/* 父容器添加如下代码 */
display: flex;
align-items: center;
/*第一个子元素*/
align-self: flex-end;

align-self: flex-end


align-self: center 居中对齐

/* 父容器添加如下代码 */
display: flex;
align-items: flex-start;
/*第一个子元素*/
align-self: center;

align-self: center


align-self: stretch 拉伸对齐

/* 父容器添加如下代码 */
display: flex;
align-items: flex-start;
/*第一个子元素 未指定高度才生效*/
align-self: stretch;

align-self: stretch

5.3 order 属性

数值越小,排列越靠前,默认为0。

① 语法:

.item {
    order: <number>;
}

② 既然默认是0,那我们将第二个子容器order:-1,那第二个元素就跑到最前面了

/* 父容器添加如下代码 */
display: flex;
/*第二个子元素*/
 order: -1;

order

六、小案例

最后我们用flex布局实现下面常见的商品列表布局

商品列表

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>简单商品布局</title>
    <style>
        .goods{
            display: flex;
            justify-content: center;
        }
        p{
            text-align: center;
        }
        span{
            margin: 0;
            color: red;
            font-weight: bold;
        }
        .goods001{
            width: 230px;
            height: 322px;
            margin-left: 5px;
        }
        .goods002{
            width: 230px;
            height: 322px;
            margin-left: 5px;
        }
        .goods003{
            width: 230px;
            height: 322px;
            margin-left: 5px;
        }
        .goods004{
            width: 230px;
            height: 322px;
            margin-left: 5px;
        }

    </style>
</head>
<body>

    <div class="goods">
        <div class="goods001">
            <img src="./imgs/goods001.jpg" >
            <p>松下(Panasonic)洗衣机滚筒</p>
            <span>¥3899.00</span>
        </div>
        <div class="goods002">
            <img src="./imgs/goods002.jpg" >
            <p>官方原装浴霸灯泡</p>
            <span>¥17.00</span>
        </div>
        <div class="goods003">
            <img src="./imgs/goods003.jpg" >
            <p>全自动变频滚筒超薄洗衣机</p>
            <span>¥1099.00</span>
        </div>
        <div class="goods004">
            <img src="./imgs/goods004.jpg" >
            <p>绿联 车载充电器</p>
            <span>¥28.90</span>
        </div>
    </div>
</body>
</html>

以上就是本期内容的全部,希望对你有所帮助。我们下期再见 (●'◡'●)

译者注

在上周我就关注到了在github上有1brc这样一个挑战,当时看到了由Victor Baybekov提交了.NET下最快的实现,当时计划抽时间写一篇文章解析他的代码实现,今天突然看到作者自己写了一篇文章,我感觉非常不错,在这里分享给大家。

这篇文章是关于.NET开发者Victor Baybekov参加的一个名为"One Billion Row Challenge"的编程挑战,他使用.NET语言编写了一个实现,这个实现的性能不仅打败了Java,甚至超过了C++。

这个挑战的目标是处理一亿行数据,并提供对数据的快速查询。原始版本只允许Java参与,但其他语言的开发者也希望参与其中,因此挑战对其他语言开放。Victor Baybekov的实现不仅在特定的数据集上表现优秀,而且在处理更通用的数据上也表现出色。他使用.NET的原因是,它的运行速度快且易于使用。

文章中,Victor Baybekov详细介绍了他的优化过程,包括使用内存映射文件,优化哈希函数,使用输入规范,使用自定义字典,优化内部循环等。他还强调了.NET的速度和易用性,同时提到了.NET提供的不安全选项,并不会使代码自动变得不安全。

对于.NET开发者来说,这篇文章提供了很多关于如何优化代码性能的实用信息。对于非.NET开发者来说,这篇文章也是一次了解.NET强大性能的好机会。

总的来说,这篇文章非常专业,为.NET开发者提供了一种思路,即通过使用.NET的功能和优化代码,可以实现非常高的性能。同时,这篇文章也证明了.NET在处理大量数据时的优秀性能和易用性。

正文

在处理真实输入数据时,.NET平台上的十亿行挑战比Java更快,甚至比C++还要快。

上周,GitHub上因为Gunnar Morling发起的“十亿行挑战”而热闹非凡。最初这是一个仅限Java参与的比赛,但后来其他语言的开发者也想加入这场乐趣。如果你不了解这个挑战及其规则,请先阅读这些链接。

https://github.com/gunnarmorling/1brc

https://github.com/gunnarmorling/1brc/discussions/categories/show-and-tell

我也被这个挑战深深吸引了。截至撰写本文时,我编写的是目前最快的托管1BRC实现版本,它不仅在大家优化的特定数据集上表现出色,而且在更通用的数据上也有很好的性能。更重要的是,我的结果在默认数据上非常接近整体最优的C++版本,并且在通用数据的情况下超过了它。

https://github.com/buybackoff/1brc

在下面的“结果”部分,我展示了不同语言和数据集的不同计时结果。在 “我的#1BRC之旅” 中,我展示了我的优化历程和性能时间线。然后我讨论了为什么.NET在编写这类代码时既快速又易用。最后,我描述了我如何在日常工作中编写高性能的.NET代码,并邀请你如果对现代且快速的.NET感兴趣,就来申请加入我们。

结果

除了我的代码之外,我还在我的家庭实验室中专门搭建了一个基准测试服务器。它拥有固定的CPU频率并且能够提供非常稳定的结果。我投入了大量的精力来比较不同实现的性能。对于.NET和Java,我测量了同一代码的JIT和AOT性能。

我没有添加排名,因为结果会根据数据的不同而有所不同。我用粗体突出显示了按语言/JIT-AOT/数据集分组的最佳结果,并用黄色背景突出显示了按数据集分组的整体最佳结果。

Results summary

https://hotforknowledge.com/2024/01/13/7-1brc-in-dotnet-even-faster-than-java-cpp/results_details.png

可能如预期的那样,C++对于默认数据集来说是最快的。然而,C++与.NET和Java之间的细微差别,即便是我也觉得有些出乎意料。我确实预料到了.NET会击败Java。这并非是第一次发生这种情况。在2016年,Aeron.NET客户端1.0版本就比Java快,我当时就在现场。

至于Rust,它很可能会成为总体的领导者。我们只需要等待直到实现是正确的。在撰写本文时,它还没有做到。

最终,所有结果应该会趋于某个物理极限和理想的CPU利用率。那么,一个有趣的问题将是,开发这样的代码付出了什么代价。对我来说,达到当前这个点相当容易,而且代码非常简单。

扩展数据集

默认的数据生成器只有少量气象站名称,最大长度低于AVX向量大小。这两个属性都有助于带来极端的性能提升。然而,规格说明中提到,可能有多达1万个独特的气象站,它们的名称最多包含100个UTF8字节。

“我鼓励参赛者尝试一下,并将其作为优化的目标。能够看到自己位于排行榜顶端无疑是令人兴奋的,但设计一个能够适应超出416个最大长度为26个字符的车站名称的解决方案更有趣(也更有用!)”

以上是Marko Topolnik的话,他最近提交了一个更通用的生成器。

为了更公平的比较,我使用了两个数据集:

原始的默认数据集是用
create_measurements.sh
生成的。它的大小超过12GB。这个数据集只有416个气象站名称,最大长度为26个字符。

扩展的数据集包含了1万个随机的气象站名称,长度可以达到规格所允许的最大值。它是用
create_measurements3.sh
生成的,大小超过17GB。详情见上面的引用链接。

在表格的底部,你可以看到一个单独的部分,用于展示那些在默认数据集上表现良好但无法正确处理1万个数据的结果。这表明这些实现使用了超出规则说明的一些假设,并且不公平地过度优化了特定的情况。例如,最快的Rust版本的作者明确表示它不适用于1万个数据。他更喜欢先编写快速的代码,然后再使其正确。

就我而言,我努力从一开始就编写最通用的代码。名称可以是任意长度,数字可以是任意非科学计数的浮点数,行尾可以是
\r\n
。就在一周前,我甚至还能用这样的代码超越顶级Java结果。

在Java再次变得更快之后(也是在短时间内),我查看了规则,但没有查看数据。对我来说,数字范围的限制是最重要的,但气象站名称仍然可以是任意长度。代码会处理冲突,但对于真实世界输入的气象站名称应该很少发生冲突。不过我必须承认,有可能创建人为数据,这些数据将会发生冲突,并将O(1)的哈希表查找变成O(N)的线性搜索。但即使在这种情况下,它仍然会工作,并且可能比参考实现还要快。也许我稍后会为了好玩而尝试这样做。

方法论

性能测试是在一个安静的6C/12T Alder Lake CPU上进行的,该CPU的频率固定在2.5 GHz(关闭睿频功能),搭配32GB DDR4 3200内存,运行Debian 12系统,并且在Proxmox的特权LXC容器中进行测试。由于基准频率是固定的,散热状况非常好(< 35°C),即使在持续100%负载下也不会发生降频现象。

时间测量使用了
hyperfine --warmup 1 --runs 5 './1brc path/to/file'
命令。由于系统中没有噪声,结果非常稳定。更多细节请查看结果表下方的链接。

对于前两名的.NET结果,我多次运行了基准测试,甚至为此重新启动了机器。确实,在默认数据上,根据JIT与AOT的不同,它们的排名有所不同。对于我的代码来说,AOT略有不利,但对于Cameron Aavik的代码来说,AOT显著提高了性能。

我的#1BRC之旅

我咳嗽已经超过2周了。新年期间咳得很厉害,以至于我在1月2日到3日请了假。1月3日,我喝着加了姜和蜂蜜的热茶,刷着Twitter。我看到了Kevin Gosse关于这个挑战的推文,我很喜欢这个想法。但我也清楚,这可能是一条通向深不见底的迷宫的入口,在那迷宫的底部,隐约能感受到曾经浪费时间的回忆。

然而,任务非常简单。我决定测量一下我写一个非常简单但仍然快速的实现需要多长时间。当时是下午1:01,到下午3:17,我就完成了第一个版本,在我的测试机上处理默认数据集/10K数据集分别需要13.5/18.0秒。然后,我开始疯狂地优化它。

通用版本,适用于任何输入

起初,我甚至没有尝试针对规格进行优化。只是一个名称和一个浮点数,中间用分号隔开,每行一个测量值,在Linux上以
\n
结束,或在Windows上以
\r\n
结束。重复1B次。

关键Idea

提交时的文件:
https://github.com/buybackoff/1brc/tree/f1b81f8a590a8a42d5be8358e6ba30489e678592/1brc

与上一版本的差异:
https://github.com/buybackoff/1brc/compare/82a17bc..f1b81f8?diff=split&w=

时间:13.490 / 17.991 (10K)

我的实现的关键思想直到最后都没有改变。魔鬼隐藏在最微小的细节中。

内存映射文件

使用mmap是显而易见的,因为我之前在高性能场景下多次使用它,比如IPC环形缓冲区。它非常简单易用,所有复杂性都由操作系统管理。最近数据库社区就是否使用mmap还是手动内存管理,即LMDB与其他方式之间进行了激烈的
讨论
。顺便说一句,我是LMDB的大粉丝,甚至为其编写了
最快的.NET封装

尽管如此,为了避免munmap的慢速时间,我在这里尝试了不使用mmap的
方法
。结果确实慢了一些,但并不太多。仅将文件复制到内存中最多需要大约200毫秒的CPU带宽,再加上不可避免的开销,这就很能说明问题了。

Utf8Span

Utf8Span
可能是实现高性能的最重要思想。它是一个结构体,存储了映射文件中UTF8段的指针和长度。数据从未被复制,即使当span作为字典中的键使用时也是如此。它从未从UTF8转换成UTF16,直到最后在排序和打印最终结果时才转换。

public readonly unsafe struct Utf8Span : IEquatable<Utf8Span>
{
    private readonly byte* _pointer;
    private readonly int _len;

    // 构造器

    public ReadOnlySpan<byte> Span => new ReadOnlySpan<byte>(_pointer, _len);

    public bool Equals(Utf8Span other) => Span.SequenceEqual(other.Span);

    // 真是太懒了!连_hash中免费可用的额外熵都没用上。
    // 但它在默认数据集上运行得相当不错。
    public override int GetHashCode()
    {
        // 使用前几个字节作为哈希值
        if (_len > 3) return *(int*)_pointer;
        if (_len > 1) return *(short*)_pointer;
        if (_len > 0) return *_pointer;
        return 0;
    }

    public override bool Equals(object? obj) => obj is Utf8Span other && Equals(other);
    public override string ToString() => new string((sbyte*)_pointer, 0, _len, Encoding.UTF8);
}

为了高效地进行哈希表查找,
Equals

GetHashCode
成为最重要的方法。

Span.SequenceEqual()
API 通常难以超越,但该调用不会内联,对于小数据来说太重了。后来我找到了一种
简单的加速
方法,但这需要对分块以及
Equals
本身进行更改。

平均值/最小值/最大值的高效更新

要计算运行平均值,我们需要存储总和和计数。这里没有什么有趣的,我们都知道,自从编程幼儿园时代起,不是吗?

更新 最小值/最大值 在数学上甚至更简单。只需检查新值是否 小于/大于 之前的 最小值/最大值 ,并相应地更新它们。然而,CPU不喜欢if语句,分支预测错误的成本很高。然而,如果你再多想一点从统计学角度来看,对于
任何稳定的过程
,实际上覆盖 最小值/最大值 的机会随着每一次观测迅速下降。即使是股票价格,它们不是稳定的,也不会每天、每月或每年都达到历史新高。温度据说“平均来说”是稳定的,并且至少在几个世纪的尺度上是稳定的。

下面是一个简单的模拟,显示了 最小值/最大值 分支所占比例的运行情况。请注意,X轴是对数的。平均来说,仅在10次观测后,两个分支都不会被采用。

Min/Max branching probabilities

最小值/最大值分支概率

这个分析告诉我们使用分支而不是更重的无分支位运算。我最终尝试了无分支的选项,但我有统计直觉,并且在第一个以及最终实现中都使用了if语句。无分支代码使得执行变得后端受限(如 perf stat 所见)。

public struct Summary
{
    // 注意,最初它们甚至不是浮点数
    public double Min;
    public double Max;
    public double Sum;
    public long Count;
    public double Average => Sum / Count;

    public void Apply(double value, bool isFirst)
    {
        // 第一个值总是会更新最小值/最大值
        if (value < Min || isFirst)
            Min = value;
        if (value > Max || isFirst)
            Max = value;
        Sum += value;
        Count++;
    }
}

.NET 默认字典

Dictionary<TKey,TValue>
几乎总是足够好的选择,也不是首先需要担心的事情。在我的案例中,它是
Dictionary<Utf8Span,Summary>
。.NET 的 JIT(即时编译器)在没有我做任何额外努力的情况下,内联了对
Utf8Span

Equals

GetHashCode
方法的调用。

还有一个非常好但不广为人知的高性能工具类
CollectionsMarshal
,用于通过引用访问字典值。其方法
GetValueRefOrAddDefault
对于更新摘要数据特别有帮助。

通过取得摘要值的引用,我们避免了将其复制和更新到栈上/栈中,然后使用常规 API 再复制回字典。记住,
Summary
是一个可变的结构体,对其引用调用方法不会导致复制。同时想象一下,如果
Summary
是一个类,那么即使使用相同的
GetValueRefOrAddDefault
,人们也必须检查空值并创建新实例的不必要开销。一个默认的结构体无需额外步骤即可准备存储数据。

// 没有结构体复制
ref var summary = ref CollectionsMarshal.GetValueRefOrAddDefault(result, nameUtf8Sp, out bool exists);
// 对于类:分支、分配、代码大小。谢谢,不用了。在 .NET 中,值类型规则。
// if (summary is null) summary = new Summary(); 
summary.Apply(value, !exists); // 这个方法在上面展示过

字节解析

对于解析字节,我只是使用了 .NET 的
Span.IndexOf

double.Parse()
API。

其他一切

性能仅取决于每个线程内的
ProcessChunk
。对于其他一切,我们可以编写任何懒惰或简单的代码。例如,我喜欢使用 LINQ/PLINQ 管道,尤其是当我能够创建一个长的和懒惰的计算时。但我可以很容易地用一个 for 循环打破这样的管道,而不需要多想,因为这对性能或可读性都无关紧要。例如,在实际的第一次提交中,聚合是在循环中进行的,仅仅因为这样想起来更简单,但完成后它被复制粘贴到了
.Aggregate()
方法中。

我很惊讶有些人准备就仅仅使用 (P/)LINQ 的事实进行争论,仅仅因为他们听说它很慢。他们显然不够了解 .NET,也没有区分热路径和冷路径。

var result = SplitIntoMemoryChunks() // 将整个 mmap 分成每个 CPU 相等的块
    .AsParallel().WithDegreeOfParallelism(_threads) // 分配到所有 CPU 核心
    .Select((tuple => ProcessChunk(tuple.start, tuple.length))) // 在每个 CPU 上执行 ProcessChunk 工作。
    .Aggregate((result, chunk) => { /* 合并结果 ... */ })
    ;

优化的浮点数解析

提交时的文件:
https://github.com/buybackoff/1brc/tree/273def1abf9c9cc365b4309a3bd8d081a3eb7951/1brc

与上一版本的差异:
https://github.com/buybackoff/1brc/compare/f1b81f8..273def1?diff=split&w=

时间:6.551 / 10.720 (10K)

在对代码进行性能分析后,我发现
double.Parse()
占用了大约57%的运行时间。字典查找占了大约24%。

我添加了一个
通用的浮点数解析器
,它有一个快速路径,但在检测到任何不规则情况时会回退到原始方法。所有的
[-]?[0-9]+[.][0-9]+
浮点数都会走这个实现的快速路径。

这几乎使性能翻了一番!还有一些其他的微优化,只需点击每个部分开头的“与上一版本的差异”链接,即可查看所有更改。

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double ParseNaive(ReadOnlySpan<byte> span)
{
    double sign = 1;
    bool hasDot = false;

    ulong whole = 0;
    ulong fraction = 0;
    int fractionCount = 0;

    for (int i = 0; i < span.Length; i++)
    {
        var c = (int)span[i];

        if (c == (byte)'-' && !hasDot && sign == 1 && whole == 0)
        {
            sign = -1;
        }
        else if (c == (byte)'.' && !hasDot)
        {
            hasDot = true;
        }
        else if ((uint)(c - '0') <= 9)
        {
            var digit = c - '0';

            if (hasDot)
            {
                fractionCount++;
                fraction = fraction * 10 + (ulong)digit;
            }
            else
            {
                whole = whole * 10 + (ulong)digit;
            }
        }
        else
        {
            // 遇到任何不规则情况就回退到完整实现
            return double.Parse(span, NumberStyles.Float);
        }
    }

    return sign * (whole + fraction * _powersPtr[fractionCount]);
}

优化的哈希函数

提交时的文件:
https://github.com/buybackoff/1brc/tree/e23c2bf8dace1450ad0411feaf54488795ec0fcb/1brc

与上一版本的差异:
https://github.com/buybackoff/1brc/compare/273def1..e23c2bf?diff=split&w=

时间:6.313 / 10.384 (10K)

它不再像
初始版本
那样懒惰,它包含了
长度
和最初的几个字节的组合。免费获得了超过3%的收益。

如果哈希总是零,我们使用线性搜索,有一些评论和最坏情况下的测量。

public override int GetHashCode()
{
    // 这里我们使用前4个字符(如果是ASCII)和长度来计算哈希。
    // 最坏的情况是前缀,如 Port/Saint 且长度相同,
    // 这对于人类地理名称来说相当罕见。

    // .NET 字典显然会因为冲突而变慢,但仍然可以工作。
    // 如果我们只保留 `*_pointer`,运行时间仍然合理,大约9秒。
    // 仅使用 `if (_len > 0) return (_len * 820243) ^ (*_pointer);` 耗时5.8秒。
    // 仅返回0 - 最糟糕的哈希函数和线性搜索 - 运行时间慢了12倍,为56秒。

    // 魔术数字820243是包含2024的最大快乐素数,来自 https://prime-numbers.info/list/happy-primes-page-9

    if (_len > 3)
        return (_len * 820243) ^ (*(int*)_pointer); // 只添加了 ^ 之前的部分
    
    if (_len > 1)
        return *(short*)_pointer;
    
    if (_len > 0)
        return *_pointer;

    return 0;
}

在这个改变之后,我开始研究哪些规则可能对性能有用。

使用输入规则

挑战的规则说明名字总是少于100个UTF8字节,最多有10K个独特的名字,温度在-99.9到99.9之间(
[-]?[0-9]?[0-9][.][0-9]
),行总是以
\n
结束。

我认为针对规则进行优化是完全可以接受的。可能有真正的气象站产生这样的数据,而代码在我出生前就已经写好了。然而,我不喜欢人们开始针对特定的数据集/生成器进行优化。因此,在这次比较中,我没有接受那些不能处理10K数据集的实现。即使使用规格,我的代码也支持任何名字长度。

将数字解析为整数

提交时的文件:
https://github.com/buybackoff/1brc/tree/e5d34c92a82a446d876089a1e1872da54bf64ebb/1brc

与上一版本的差异:
https://github.com/buybackoff/1brc/compare/e23c2bf..e5d34c9?diff=split&w=

时间:5.229 / 8.627 (10K)

仅仅利用温度在-99.9到99.9之间的事实。我们只有4种情况,可以为此进行优化:

...;-99.9
...;-9.9
...;9.9
...;99.9

设置字典容量

提交时的文件:
https://github.com/buybackoff/1brc/tree/3644b251cda38abd620bda644efda12951020042/1brc

与上一版本的差异:
https://github.com/buybackoff/1brc/compare/e5d34c9..3644b25?diff=split&w=#

时间:4.341 / 8.951 (10K)

这真是太傻了!但在我迫切需要提升性能的时候,这就像罐头食品一样珍贵。仅仅一行代码/改动五个字符就能获得17%的性能提升。

优化的IndexOf

提交时的文件:
https://github.com/buybackoff/1brc/tree/7fdd17a755665910ecfabb4667b5bda277531e39/1brc

与上一版本的差异:
https://github.com/buybackoff/1brc/compare/3644b25..7fdd17a?diff=split&w=#diff-50d5d1069929df17bbf6f330e04035cfaafa17de2e48ab86ce2dbd0de338528aR99-R125

时间:4.040 / 8.609 (10K)

在剩余部分小于32字节时,手动AVX2在Span中搜索字节,并回退到
Span.IndexOf

// 在 Utf8Span 内部
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal int IndexOf(int start, byte needle)
{
    if (Avx2.IsSupported)
    {
        var needleVec = new Vector<byte>(needle);
        Vector<byte> vec;
        while (true)
        {
            if (start + Vector<byte>.Count >= Length)
                goto FALLBACK;
            var data = Unsafe.ReadUnaligned<Vector<byte>>(Pointer + start);
            vec = Vector.Equals(data, needleVec);
            if (!vec.Equals(Vector<byte>.Zero))
                break;
            start += Vector<byte>.Count;
        }

        var matches = vec.AsVector256();
        var mask = Avx2.MoveMask(matches);
        int tzc = BitOperations.TrailingZeroCount((uint)mask);
        return start + tzc;
    }

    FALLBACK:

    int indexOf = SliceUnsafe(start).Span.IndexOf(needle);
    return indexOf < 0 ? Length : start + indexOf;
}

积极的使用本机整数

提交时的文件:
https://github.com/buybackoff/1brc/tree/d6c8e48b07821a05a1582f0e98f949360e3b4bd9/1brc

与上一版本的差异:
https://github.com/buybackoff/1brc/compare/7fdd17a..d6c8e48?diff=split&w=

时间:3.693 / 8.604 (10K)

在本机环境中,使用size_t本机大小类型作为偏移和长度是正常的,因为CPU处理本机字更快。在.NET中,大多数公共API接受32位int。CPU必须每次将其扩展为nint。但内部.NET本身使用本机整数。

例如,这是带有注释的
SpanHelpers.SequenceEqual
代码:

// 优化的基于字节的SequenceEquals。这个的“length”参数被声明为nuint而不是int,
// 因为我们也用它来处理除byte以外的类型,其中长度一旦通过sizeof(T)缩放就会超过2Gb。
[Intrinsic] // 对常量长度展开
public static unsafe bool SequenceEqual(ref byte first, ref byte second, nuint length)
{
    bool result;
    // 使用nint进行算术运算以避免不必要的64->32->64截断
    if (length >= (nuint)sizeof(nuint))

使用自定义字典

提交时的文件:
https://github.com/buybackoff/1brc/tree/8841e83e2abfb5f57a872cbea4c979c9b9e49178/1brc

与上一版本的差异:
https://github.com/buybackoff/1brc/compare/d6c8e48..8841e83?diff=split&w=

时间:3.272 / 8.232 (10K)

直到这一点,我仍然使用的是默认的.NET字典。但由于规格说明最多有10K个独特的名字,我可以利用这个规则。

详细信息以后再补充。

快速 Utf8Span.Equals

提交时的文件:
https://github.com/buybackoff/1brc/tree/9ed39221ec7db8f89e8e2a0702d43a184cc5e879/1brc

与上一版本的差异:
https://github.com/buybackoff/1brc/compare/8841e83..9ed3922?diff=split&w=

时间:2.773 / 6.635 (10K)

我花了一些努力尝试击败
Span.SequenceEqual
在小尺寸上的性能。尝试复制实现的部分并内联它,但没有任何效果。然后我有了一个疯狂的想法,允许代码读取超出
Utf8Span.Length
的内容。然后我可以只使用一个 AVX2 向量,将长度之后的字节设置为零,并比较向量。这将是完全不安全的,并且会导致段错误,但只是在十亿个观测值中的最后一个单独观测值中。

为了确保安全,我确保最后一个大块不是在文件末尾结束,而是至少在距离末尾
4 x Vector256<byte>.Count
的新行开始处结束。我将剩余部分复制到一个比数据大得多的内存缓冲区中,这是安全使用的。

优化内循环

提交时的文件:
https://github.com/buybackoff/1brc/tree/1051e06052d5a8a95fa0aee461e37d969532aa65/1brc

与上一版本的差异:
https://github.com/buybackoff/1brc/compare/9ed3922..1051e06?diff=split&w=

时间:2.204 / 4.811 (10K)

  • 更快的整数解析结合新行索引计算;
  • 更快的 IndexOf,也依赖于读取超出 Utf8Span.Length 的内容;
  • 更快的 ProcessChunk 循环。

详细信息待定

性能时间线

以下是讨论上述每次更改后性能演变的时间线。

Performance timeline

.NET 非常快

.NET 非常快。而且每个新版本都在变得更快。有些人开玩笑说,对于 .NET 的最佳性能优化就是更新它 - 对于大多数用户来说,这可能是真的。

每次发布新版本时,.NET 团队的 Stephen Toub 都会发表一篇巨大的博客文章,介绍自上次发布以来的每一个微小性能改进。这些文章的庞大体量表明,他们非常关心性能的提升。

不安全代码

.NET 允许你直接使用指针。这使得它类似于 C 语言。如果内循环受 CPU 限制,所有数组都可以被固定并在没有边界检查的情况下访问,或者我们可以直接像在这个 1BRC 案例中那样直接处理本地内存。

另外,.NET 提供了一个较新的 Unsafe 类,它本质上与旧的 unsafe 关键字 + 指针做同样的事情,但使用托管引用。这允许跳过固定数组,同时仍然是 GC 安全的。

不安全选项的存在并不会自动使代码不安全。有“不安全的 Unsafe”和“安全的 Unsafe”。例如,如果我们确保数组边界,但不能使 JIT 省略边界检查(如在自定义字典案例和
GetAtUnsafe
中),那么为什么我们要支付边界检查的成本呢?在这种情况下,它将是安全的 Unsafe。通过谨慎使用,局部不安全的代码可以变成全局安全的应用程序。

易用的向量化函数

.NET 有非常容易使用的 SIMD 内在函数。我们可以直接使用 SSE2/AVX2/BMI API,或者使用跨平台跨架构的
Vector128<T>
/
Vector256<T>
类型。或者更通用的
Vector<T>
类型,它甚至隐藏了向量大小,并且可以在旧的 .NET 运行时上无缝工作。

.NET 的范围

.NET 不强迫我们每次都编写低级的不安全 SIMD 代码。当性能不重要时,我们可以只使用 LINQ。这很好。即使在这个 1BRC 挑战中也是如此。真的。

C# 与 F#

F# 在默认数据集和10K数据集上都展现出了不俗的性能。我与 F# 的关系颇为复杂。博客上的一篇长篇文章讲述了我为何放弃 F# 转而选择 C# 的原因。主要是因为性能问题(包括生成的代码和工具的性能),尽管我喜欢 F# 的语法和社区。

然而,F# 的速度之快并不让我感到惊讶。它一直在稳步提升,或许有一天我会再次使用 F#. 例如,可恢复代码和可恢复状态机是我一直在关注的非常强大的功能。.NET 原生支持的
task { ... }
计算表达式就利用了这一特性。

在这里,我不得不提到,我也通过一系列在2020年的提交,大幅提高了 F# 性能,使其核心的
Map

Set
数据结构(内部是 AVL 树)的速度大大加快。


当然,正如作者所承认的,Frank Krueger 的 F# 实现远非典型的函数式 F# 代码。但是,如果你已经在使用 F# 代码,而且不想碰 C#,你也可以在 F# 中写类似 C 的代码。只是不要过度,把它隐藏在纯函数里,然后对外保密。

当开发者谈论开源时,通常会想到 GitHub,它不仅仅是一个代码托管平台,更是一个汇聚了全球开发者的社交中心。过去,开发者发布一款软件后,都是在自己的小圈子里默默努力和交流,现在通过 GitHub 平台可以方便地与全球的开发者分享、交流和协作。贡献者在这里展示自己的才华,追随者在这里寻找强者的脚印,等待着被世人认可的时刻。

更多人在谈到开源时,会提到“免费”,正是上面的这些人用爱发电,才让开源成为免费的宝库,如果理解不了他们的热爱,请不要伤害。

下面,让我们一起看看,过去一周开源领域都发生了什么,关注开源最新动态、品热搜开源项目。

  • 本文目录
    • 1. 开源新闻
      • 1.1 LSPosed 宣布停更
      • 1.2 锤子开源软件 One Step 疑被抄袭
      • 1.3 PyPy 迁移到 GitHub
    • 2. GitHub 热搜项目
      • 2.1 安卓内核级的 root 方案
      • 2.2 神奇的 shell 历史记录工具
      • 2.3 手绘风格的白板
      • 2.4 人人都能用英语
      • 2.5 AI 机器人
    • 3. HelloGitHub 热评
      • 3.1 (no)SQL 数据库桌面管理工具
      • 3.2 一款电脑上的广告拦截器
    • 4. 往期回顾

1. 开源新闻

1.1 LSPosed 宣布停更

LSPosed 是一款运行于 Android 操作系统的钩子框架,支持 Android 8.1 ~ 14 版本。它能够拦截几乎所有 Java 函数的调用,从而可被用来修改 Android 系统和软件的功能。

近期,该项目作者因为在其用户交流群遭受了大量辱骂和人身攻击,所以决定暂停 LSPosed 的开发和维护。

GitHub 地址→
https://github.com/LSPosed/LSPosed

1.2 锤子开源软件 One Step 疑被抄袭

开源项目 One Step 可以是通过拖拽的方式,完成将信息发送至应用或联系人的动作,节省了在不同应用之间切换的诸多步骤,打通了 Android 设备上应用间的边界。

近日,罗永浩其辟谣号在微博质疑荣耀抄袭锤子手机的 One Step(一步)功能。

GitHub 地址→
https://github.com/SmartisanTech/android

1.3 PyPy 迁移到 GitHub

PyPy 是一种 Python 语言实现的解释器,因为其采用了 JIT(即时编译器)可以提前将 Python 代码提前编译成机器码,所以相较于官方的 CPython 更快、更节省内存,但启动时需要更长的时间。

近期,PyPy 已从 Mercurial、Heptapod 迁移到 Git、GitHub,原因如下:

  1. 更多的流量,GitHub 已成为开源的代名词
  2. 方便贡献和追踪问题
  3. 兼容 Mercurial(轻量级分布式版本控制软件,主要由 Python 语言实现)
  4. 更丰富的服务,比如 CI

GitHub 地址→
https://github.com/pypy/pypy

2. GitHub 热搜项目

2.1 安卓内核级的 root 方案:KernelSU

主语言:Kotlin

Star:6.2k

周增长:300+

这是 Android 的 root 解决方案,它工作在内核模式,可直接在内核空间中为用户空间应用程序授予 root 权限,支持 GKI 2.0 的设备(内核版本 5.10 以上)。

GitHub 地址→
https://github.com/tiann/KernelSU

2.2 神奇的 shell 历史记录工具:atuin

主语言:Rust

Star:1.4w

该项目通过 SQLite 数据库存储 shell 历史,能够显示更多的 shell 历史、命令运行时间、执行时间等信息,还支持选择、过滤、统计、同步/备份等操作。

GitHub 地址→
https://github.com/atuinsh/atuin

2.3 手绘风格的白板:excalidraw

主语言:TypeScript

Star:6.4w

周增长:1.6k

这是一款完全免费、开源的基于无限画布的白板 Web 应用,用户可以在上面创建手绘风格的作品。支持包括中文在内的多种语言,提供了自由绘制、多种工具、导出 PNG、实时协作、共享链接、自动保存等功能。

GitHub 地址→
https://github.com/excalidraw/excalidraw

2.4 人人都能用英语:everyone-can-use-english

Star:1.2w

增长:1k

这本书,只是把 “正确的事情” 聚焦在 “用英语” 上,而后再看看可能的 “正确的方式” 究竟是什么。

GitHub 地址→
https://github.com/xiaolai/everyone-can-use-english

2.5 AI 机器人:mobile-aloha

主语言:Python

Star:2.7k

增长:1.1k

这是一个低成本的全身远程操作系统,它可以学习人类的操作行为,比如操作员演示 50 次煎炒虾,该机器人就可以自主完成炒虾的操作,作者团队还演示了擦玻璃、洗碗、收纳物品的任务。

GitHub 地址→
https://github.com/MarkFzp/mobile-aloha

3. HelloGitHub 热评

在这个章节,将会分享下本周 HelloGitHub 网站上的热门开源项目,欢迎与我们分享你上手这些开源项目后的使用体验。

3.1 (no)SQL 数据库桌面管理工具:dbgate

主语言:Svelte

这是款免费、开源的数据库桌面管理工具,支持包括 MySQL、PostgreSQL、SQL Server、MongoDB、SQLite、Redis 等多种数据库,适用于 Windows、Linux、macOS 系统。

项目详情→
https://hellogithub.com/repository/8eed358dbe504fb284df3b7953fc62f5

3.2 一款电脑上的广告拦截器:zen

主语言:Go

该项目是采用 Go 语言 Wails 框架写的能够屏蔽各种广告的桌面工具。它的工作原理是设置一个代理,拦截所有应用的 HTTP 请求,从而阻止广告和跟踪行为的请求,支持 Windows、macOS 和 Linux 系统。

项目详情→
https://hellogithub.com/repository/11df295cef134696acb63c22218f503c

4. 往期回顾

往期回顾:


以上为 2024 年第 3 个工作周的 GitHub Trending

开心一刻

下午正准备出门,跟正刷着手机的老妈打个招呼

我:妈,今晚我跟朋友在外面吃,就不在家吃了

老妈拿着手机跟我说道:你看这叫朋友骗缅北去了,tm血都抽干了,多危险

我:那是他不行,你看要是吴京去了指定能跑回来

老妈:还吴京八经的,特么牛魔王去了都得耕地,唐三藏去了都得打出舍利,孙悟空去了都得演大马戏

我:那照你这么说,唐僧师徒取经走差地方了呗

老妈:那可没走错,他当年搁西安出发,他要是搁云南出发呀,上午到缅北,下午他就到西天

我:哈哈哈,那西游记就两级呗,那要是超人去了呢?

老妈:那超人去了,回来光剩超,人留那了

问题复现

我简化下业务与项目

数据库:
MySQL
8.0
.
25

基于
spring-boot 2.2.10.RELEASE
搭建
demo

spring-boot-jpa-demo

表:
tbl_user

测试代码:


/*** @description: xxx描述
*
@author: 博客园@青石路
* @date: 2024/1/9 21:42
*/@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public classUserTest {

@Resource
privateUserRepository userRepository;

@Test
public voidget() {
DateTimeFormatter dft
= DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
Timestamp lastModifiedTime
= Timestamp.valueOf(LocalDateTime.parse("2024-01-11 09:33:26.643", dft));//1.先保存一个user User user = newUser();
user.setUserName(
"zhangsan");
user.setPassword(
"zhangsan");
user.setBirthday(LocalDate.now().minusYears(
25));
user.setLastModifiedTime(lastModifiedTime);
log.info(
"user.lastModifiedTime = {}", user.getLastModifiedTime());
userRepository.save(user);
log.info(
"user 保存成功,userId = {}", user.getUserId());//2.然后再根据id查询这个user Optional<User> userOptional =userRepository.findById(user.getUserId());if(userOptional.isPresent()) {
log.info(
"从数据库查询到的user,user.lastModifiedTime = {}", userOptional.get().getLastModifiedTime());
}
}
}

View Code

这么清晰的代码,大家都能看懂吧?

我们来看下日志输出

保存的时候,
lastModifiedTime
的值是
2024-01-11 09:33:26.643
,从数据库查询得到的却是:
2024-01-11 09:33:27.0

是不是被震惊到了?

曲折排查

先确认下
MySQL
表中存的值是多少

数据库表中的值就是
2024-01-11 09:33:27
,此刻我只想来一句:卧槽!

这说明数据入库有问题,而不是读取有问题

我们来梳理下数据入库经历了哪些环节

那问题肯定出在
Spring Data JPA

mysql-connector-java
之间

MySQL
肯定是没问题的!

源码跟踪

既然问题出在
Spring Data JPA

mysql-connector-java
之间,那么我们就直接来个一穿到底,翻了它的源码老底

大家请坐好,我要开始装逼了

JPA
用的少,一时还不知道从哪里开始去跟源码,但不要慌,楼主有
葵花宝典

杂谈篇之我是怎么读源码的,授人以渔

断点追踪源码,一时用一时爽,一直用一直爽

直接在
userRepository.save(user)
前面打个断点,然后一步一步往下跟,我就不细跟了,我只在容易跟丢的地方指出来,给你们合适的方向

当断点到
SessionImpl#firePersist
方法时

我们应该去跟
PersistEventListener::onPersist
了,一路跟下去,会来到
AbstractSaveEventListener#performSaveOrReplicate
方法

里面有如下代码

添加的
Action
的实际类型是:
EntityIdentityInsertAction

这里涉及到了
hibernate

事件机制
,简单来说就是
EntityIdentityInsertAction

execute
方法会被调用

所以我们继续从
EntityIdentityInsertAction#execute
跟,会来到
GetGeneratedKeysDelegate#executeAndExtract

重点来了,大家打起精神

继续跟进
session.getJdbcCoordinator().getResultSetReturn().executeUpdate( insert )

executeUpdate

它长这样

如果不是断点跟的话

你知道接下来跟谁吗?

当然,非常熟悉源码的人(比如我),肯定知道跟谁

但是用了断点,大家都知道跟谁了

继续往下跟,当我们来到
ClientPreparedStatement#executeInternal
时,真相已经揭晓

此时已经来到了
mysql-connector-java
,发送给
MySQL Server

SQL
是:

last_modified_time
精度没丢

那问题出在哪?

还能出在哪,
MySQL
呗!

说好的
MySQL
没问题的了?

MySQL 时间精度

用排除法,排的只剩
MySQL
了,直接执行
SQL
试试

哦豁,敢情前面的源码分析全白分析了,我此刻的心情你们懂吗

这必须得找
MySQL
要个说法,真是太狗了

我们去
MySQL
官方文档找找看(注意参考手册版本要和我们使用的
MySQL
版本一致)

大家不要通篇去读,那样太费时间,直接
search
用起来

The DATE, DATETIME, and TIMESTAMP Types
有这么一段比较关键

我给大家翻译一下

继续看
Fractional Seconds in Time Values
,内容不多,大家可以通篇读完

MySQL

TIME

DATETIME

TIMESTAMP
都支持微妙级别(6位数)的小数位

精度直接在括号中指定,例如:
CREATE TABLE t1 (t TIME(3), dt DATETIME(6))

小数位的范围是 0 到 6。0 表示没有小数部分,如果小数位缺省,则默认是0(
SQL规范规定的默认是 6,MySQL8 默认值取 0 是为了兼容 MySQL 以前的版本

当插入带有小数部分的
TIME

DATETIME

TIMESTAMP
值到相同类型的列时,
如果值的小数位与精度不匹配时,会进行四舍五入

四舍五入的判断位置是精度的后一位,比如精度是 0,则看值的第 1 位小数,来决定是舍还是入,如果精度是 2,则看值的第 3 位小数

简单来说:值的精度大于列类型的精度,就会存在四舍五入,否则值是多少就存多少

当发生四舍五入时,既不会告警也不会报错,因为这就是 SQL 规范

那如果我不像要四舍五入了,有没有什么办法?

MySQL
也给出了支持,就是启用
SQL mode

TIME_TRUNCATE_FRACTIONAL

启用之后,当值的精度大于列类型的精度时,就是直接按列类型的精度截取,而不是四舍五入

那这么看下来,不是
MySQL
的锅呀,
MySQL
表示这锅我不背

那是谁的锅?

只能说是开发人员的锅,为什么不按
MySQL
使用说明书使用?

我要强调的是,产生这次问题的代码不是我写的,我写的代码怎么可能有
bug

总结

1、 源码
debug
堆栈

2、MySQL 时间精度

MySQL

TIME

DATETIME

TIMESTAMP
类型都支持微妙级别(6位数)的精度

默认情况下会四舍五入,若想直接截断,则需要开启
SQL mode

TIME_TRUNCATE_FRACTIONAL

3、规范

阿里巴巴的开发手册中明确指出不能用:
java.sql.Timestamp

另外很多公司的
MySQL
开发规范会强调:没有特殊要求,时间类型用
datetime

主要出于两点考虑:1、
datetime
可用于分区,而
timestamp
不行,2、
timestamp
的范围只到
2038-01-19 03:14:07.499999

有的开发小伙伴可能会问:如果到了
2038-01-19 03:14:07.499999
之后,
timestamp
该怎么办?

我只能说:小伙子你想的太远了,
2038
跟我们有什么关系,影响我们送外卖吗?

今年.NET Conf China 2023技术大会,我给大家分享了

.NET应用国际化-AIGC智能翻译+代码生成的议题,今天整理成博客,分享给所有人。
随着疫情的消退,越来越多的企业开始向海外拓展,应用系统的国际化和本地化是一个巨大的技术挑战,我们今天重点探讨以下内容:
  1. .NET应用如何实现国际化?不仅仅包含资源文件和文本的替换,还有文本词条抽取、智能翻译、代码替换、本地化处理等各种场景。
  2. 基于Roslyn进行代码分析,查找中文文本、抽取词条,以及代码替换。
  3. 机器翻译与GPT的Battle,基于GPT4实现一个智能翻译服务。
  4. 彩蛋环节:如何使用Github Copilot自动生成代码和单元测试。
一、.NET应用如何实现国际化
目前我们的充电服务平台包含16大子系统,上千个功能菜单,数十个数据库... 大部分应用基于.NET技术栈构建,都需要支持国际化&本地化。 如何快速、高效、准确地完成产品国际化&本地化改造是团队面临的一个巨大的挑战! 没有相关经验怎么办? 好在有了ChatGPT!!! 我们先问一下ChatGPT~

总结:
技术方案可行,但是有几个架构设计上的问题: 1. 重度依赖资源文件 2. 大型分布式部署,分发、管理成本很高、很复杂 3. 需要大范围扫描、改造代码 4. 翻译、校对工作量巨大 5. 无法批量、动态修改翻译文本。
在ChatGPT基础上,发挥.NET技术栈的能力,创新设计了一个新的解决方案:
抽象封装一个词条服务,根据线程上下文的CurtureInfo,动态获取对应的多语言文本 注:
1.1 词条通常用于标识需要被翻译文本的唯一标识。

上图中: 词条类I18NTerm:用于存储词条数据 词条管理接口:用于词条的批量新增、修改 词条服务接口:支持按词条查询对应的翻译文本。

2 基于Roslyn,解析代码中的中文,形成多语言词条,同时做代码替换 封装一个翻译服务,批量翻译词条

再次找到ChatGPT问一问 Prompt:你是一个.NET资深开发工程师,全面掌握C#语言,请基于Roslyn技术实现一个服务,输入一个sln解决方案的路径,扫描各个类中的中文文字,统一替换为I18nTermService.GetText('词条ID')

重新设计一下技术实现方案:

3. 机器翻译与GPT的Battle,基于GPT4实现一个智能翻译服务。

基于Azure AI services 的 Translator实现机器翻译

但是机器翻译的准确性怎么样?机器翻译有哪些问题

尝试使用ChatGPT做专业翻译:先设计Prompt 请把以下词语列表翻译为英文 1. 充电站,2.电站,3.充电桩,4.充电终端,5.终端,每个单词一行

依旧不理想,继续修改Prompt
Prompt:
你是一个美国电动汽车充电服务运营商,精通中文和英文,请使用专业领域术语,把以下词语列表翻译为英文,1. 充电站,2.电站,3.充电桩,4.充电终端,5.终端,翻译时电站等同于充电站,充电终端等同于充电桩,终端也等同于充电桩,每个单词一行。

充电桩的专业翻译是 Charging point 需要一个专业术语表

继续改进Prompt
Prompt:你是一个美国电动汽车充电服务运营商,精通中文和英文,请使用以下格式的专业术语 {"充电站":"Charging station", "充电桩":"Charging point"},把以下词语列表翻译为英文,1. 充电站,2.电站,3.充电桩,4.充电终端,5.终端,翻译时电站等同于充电 站,充电终端等同于充电桩,终端也等同于充电桩,每个单词一行。

翻译准确性提升了 我们继续改进,同时实现工程化
Prompt:你是一个美国电动汽车充电服务运营商,精通中文和英文,请使用以下格式的专业术语 {"充电站":"Charging station", "充电桩":"Charging point"},把以下词语列表翻译为英文,1. 充电站,2.电站,3.充电桩,4.充电终端,5.终端,翻译时电站等同于充电站,充电终端等同于充电桩,终端也等同于充电桩,请以JSON格式返回,例如 {"充电站":"Charging station", "充电终端":"Charging point"},不需要做解释

更好的Prompt
请扮演一个美国电动汽车充电服务运营商,精通中文和英文,请使用以下专业术语 {"充电站":"Charging station", "电站":"Charging station", "场站":"Charging station", "充电桩":"Charging point", "充电终端":"Charging point", "终端":"Charging point" , "电动汽车":"Electric Vehicle", "直流快充":"DC Fast Charger","超级充电站":"Supercharger","智能充电":"Smart Charging","交流慢充":"AC Slow Charging"}, 把请将用户的输入翻译为英文, 请以JSON格式返回 例如 {"充电站":"Charging station", "充电终端":"Charging point"} 不需要做解释

1.3 Prompt搞定后,使用SK框架,基于GPT4实现翻译服务, 用于专业翻译

测试一下:

4. 彩蛋环节:如何使用Github Copilot自动生成代码和单元测试。

除了国际化翻译之外,我们还需要做应用的本地化处理。例如: 提供一个公共的本地化组件,支持对数字、时间、度量衡在不同区域下的处理。 接下来分享团队基于Github Copilot开发副驾,示例完成以上代码的生成过程。

先看一下Github copilot

Prompt: 请用C#生成一个提供度量衡服务的实现类MeasurementService,它提供了以下方法将长度值转换为英寸、长度值转换为英尺、 将长度值转换为英里、 将长度值转换为厘米、 将长度值转换为千米、 将重量值转换为克、 将重量值转换为千克、 将功率值转换为瓦特、 将电流值转换为安培、 将电压值转换为伏特。 例如将长度值转换为英寸的实现方法是public double ConvertToInch(double value, LengthUnit lengthUnit),这个方法中遍历LengthUnit,做长度转换。方法请添加标准注释,使用中文注释。

这里你会发现,其他方法未实现,需要继续告诉Github Copilot继续生成代码 Github Copilot生成的代码不一定全面准确,需要做代码确认,这个很关键 Prompt: MeasurementService类中, 请参考ConvertToInch这个方法的实现方式和注释要求, 继续生成ConvertToMile,ConvertToFoot,ConvertToCentimeter, ConvertToKilometer, ConvertToGram,ConvertToKilogram, ConvertToWatt,ConvertToAmpere, ConvertToVolt等方法的具体转换逻辑, 每个方法都要实现代码输出。
我们继续让Github Copilot生成单元测试代码:
首先选择整个类,然后输入以下Prompt Prompt: @workspace /tests 请对选中的代码,使用MSTest单元测试框架,生成单元测试代码,请为每个方法都实现单元测试

以上我们共同探讨了基于AIGC实现.NET应用国际化 从智能翻译到代码生成,
这是LLM时代一个小小的案例,但是 未来: 有LLM加持的智能翻译将更精准,全面提升用户体验。
代码自动生成将全面释放开发者创造力。
随着AIGC的迭代升级,AI将为我们带来更多应用创新和价值创造。
周国庆
2024/1/5