<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh"><generator uri="https://jekyllrb.com/" version="3.8.5">Jekyll</generator><link href="https://mechanical-consciousness.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mechanical-consciousness.com/" rel="alternate" type="text/html" hreflang="zh" /><updated>2020-04-07T19:11:18+08:00</updated><id>https://mechanical-consciousness.com/feed.xml</id><title type="html">机械意志</title><subtitle>吾等一众凡夫俗子，愿以有生之年，待机械意志之觉醒、降临。
</subtitle><author><name>Chizhong Jin</name><email>pjincz@gmail.com</email></author><entry><title type="html">KVM虚拟机GPU直通，step by step</title><link href="https://mechanical-consciousness.com/2020/03/20/kvm-gpu-passthrough.html" rel="alternate" type="text/html" title="KVM虚拟机GPU直通，step by step" /><published>2020-03-20T00:00:00+08:00</published><updated>2020-03-20T00:00:00+08:00</updated><id>https://mechanical-consciousness.com/2020/03/20/kvm-gpu-passthrough</id><content type="html" xml:base="https://mechanical-consciousness.com/2020/03/20/kvm-gpu-passthrough.html">&lt;p&gt;最近工作的事情有点烦躁，就开始打酱油了。折腾了好久，可算把家里的nas和游戏机合为一体了。&lt;/p&gt;

&lt;p&gt;这其中最蛋疼的步骤可算是要在Linux虚拟机上跑游戏了。在无数次死机重启之后，可算把这个问题搞定的。&lt;/p&gt;

&lt;p&gt;网上GPU虚拟机直通的教程不少，但是都有一个问题，就是只告诉你怎么一步一步做，然后启用了一个又一个Linux模块，但是往往搞到最后一启动虚拟机就整个电脑死机了（囧，对的，整个电脑死机，不是虚拟机死机）。&lt;/p&gt;

&lt;p&gt;踩了n天的坑后，总算把GPU直通的所有坑都踩了一遍。也都看了一遍。这里记录下来，有兴趣的朋友一起研究研究吧。&lt;/p&gt;

&lt;p&gt;整个虚拟机GPU直通大约有那么几个步骤（缺1不可），我的平台是Intel + Nvidia，因此用AMD的朋友可能会有所不同。但是整体思路应该是一致的。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;确认硬件支持&lt;/p&gt;

    &lt;p&gt;主要是CPU和主板需要支持VT-d技术，如果是AMD平台的话，应该是AMD-Vi支持。
具体行不行，直接Google之。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;买买买&lt;/p&gt;

    &lt;p&gt;如果你只有一张显卡的话，GPU直通显然是没法玩的，显卡分配给了虚拟机，宿主机怎么办？&lt;/p&gt;

    &lt;p&gt;当然你可以搞两张一样的卡，我个人的话直接搞了张二手GTX 650，便宜实惠。反正宿主机对显卡也没啥要求。&lt;/p&gt;

    &lt;p&gt;理论上来说A卡N卡混合应该无所谓，但我没试过。&lt;/p&gt;

    &lt;p&gt;理论上来说，如果你有集成显卡，有些主板有选项，可以默认从集成显卡启动，这样的话你可以省掉一张显卡。（很遗憾i9连核显都没有）
省掉一张显卡主要不是为了省钱啊，我现在的机箱已经重到搬不动了，两张显卡，3块大硬盘，2个SSD，还有水冷啥的，塞的满满当当的，囧。&lt;/p&gt;

    &lt;p&gt;另外推荐入手一个kvm切换器，等显卡直通搞定了，你就相当与有了两台电脑了，没个kvm不得烦死？&lt;/p&gt;

    &lt;p&gt;另外，如果主板的所有USB口都在一个iommu group的话，推荐再入手个PCI-E的usb扩展卡。后面会仔细解释这个事情。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;在BIOS中调整相关参数&lt;/p&gt;

    &lt;p&gt;首先肯定是要启用VT-d的啦，（AMD的话就是AMD-Vi）&lt;/p&gt;

    &lt;p&gt;然后还有一个重要选项是默认从哪张显卡输出。可惜不是所有BIOS都支持这个选项，我的老板子就不支持，前端时间坏了（真假），被我顺利淘汰了。&lt;/p&gt;

    &lt;p&gt;如果有这个选项的话，就可以把好显卡插在主PCI-E槽，然后默认从差的显卡启动。如果没有的话，想开点，交换一下吧。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;在宿主系统中启用iommu&lt;/p&gt;

    &lt;p&gt;iommu是GPU虚拟机直通的核心。&lt;/p&gt;

    &lt;p&gt;虽然我们总是在讲GPU直通，其实本质上来说，通过VT-d技术，各种乱七八糟的东西都可以丢到虚拟机里面去，让虚拟机直接访问。&lt;/p&gt;

    &lt;p&gt;iommu负责把主板上的设备分组，分成一个一个group。每个group都可以单独的启用或者停用。&lt;/p&gt;

    &lt;p&gt;在没启用iommu之前，宿主系统会占用全部的硬件。在启用iommu之后，宿主系统可以有选择的预留一些硬件资源，把这些硬件资源分配给虚拟机。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;在宿主系统中预留iommu group&lt;/p&gt;

    &lt;p&gt;无论有没有启用iommu，Linux默认会扫描系统中的所有硬件，并尝试给每个硬件分配驱动，并初始化。&lt;/p&gt;

    &lt;p&gt;这个事情的直接后果就是，系统启动完了后，iommu倒是启用了，但是没有闲着的分组？？？&lt;/p&gt;

    &lt;p&gt;所以我们需要在Linux内核启动足够早的阶段把硬件预留下来。&lt;/p&gt;

    &lt;p&gt;这个步骤是整个事情中最麻烦的，也最有可能遇到问题的。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;创建kvm虚拟机，并将iommu group分配给虚拟机&lt;/p&gt;

    &lt;p&gt;这个简单&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;给虚拟机安装系统&lt;/p&gt;

    &lt;p&gt;emmm….&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;阻止nvidia驱动发现虚拟机&lt;/p&gt;

    &lt;p&gt;这个步骤我是真的真的想吐槽，所有一切都就绪了之后，系统起来被锁定在800x600的分辨率，驱动提示错误35。&lt;/p&gt;

    &lt;p&gt;刚开始我始终以为是自己哪里做错了。结果上网一搜发现，竟然是nvidia的驱动直接去侦测自己是不是在虚拟机里跑的。如果是的话就直接报错。&lt;/p&gt;

    &lt;p&gt;修正这个问题不难，kvm自身有特性可以隐藏虚拟机，直接打开就能解决这个问题。但是这破问题坑了我好几个小时。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;安装steam, 安装游戏, 加我steam好友&lt;/p&gt;

    &lt;p&gt;[狗头]&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;总体来说就这么多步骤。其中大部分的步骤都不难，最有挑战的部分应该是预留iommu group。因为每个人的硬件不一样，遇到的情况也不一样。&lt;/p&gt;

&lt;p&gt;本文不覆盖买买买，因为不打广告。
本文也不覆盖BIOS设置，因为我这非主流主板，覆盖了大家也没兴趣啊。&lt;/p&gt;

&lt;p&gt;我们直接从如何启用iommu开始，一步一步的讲解，解完一步确认一步。&lt;/p&gt;

&lt;p&gt;本文使用的操作系统为Ubuntu 18.04，其实系统影响不大，主要还是内核和硬件。但是不少步骤是系统相关的，例如如何修改grub启动参数等，如果在其他系统上实施，需要做相应的动作。&lt;/p&gt;

&lt;h3 id=&quot;在宿主系统中启用iommu&quot;&gt;在宿主系统中启用iommu&lt;/h3&gt;

&lt;p&gt;在宿主系统中启用iommu很简单，就是给Linux内核一个额外的启动参数。&lt;/p&gt;

&lt;p&gt;对Ubuntu来说，我们只需要修改&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/default/grub&lt;/code&gt;配置文件即可。&lt;/p&gt;

&lt;p&gt;打开&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/default/grub&lt;/code&gt;，找到&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GRUB_CMDLINE_LINUX_DEFAULT&lt;/code&gt;，默认情况下这行大概是这样的：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GRUB_CMDLINE_LINUX_DEFAULT=&quot;quiet splash&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;我们把这行改为：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GRUB_CMDLINE_LINUX_DEFAULT=&quot;quiet splash intel_iommu=on&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如果是AMD的话，据说是加&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;amd_iommu=on iommu=pt kvm_amd.npt=1 kvm_amd.avic=1&lt;/code&gt;，没实验过，请自行尝试。&lt;/p&gt;

&lt;p&gt;接着我们需要更新grub：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ sudo update-grub -u
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后重启电脑，重启完后我们需要检查iommu有没有被正确启用：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ dmesg | grep -i iommu
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如果你在输出中看到很多&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pci xxxx:xx:xx.x: Adding to iommu group xx&lt;/code&gt;，那么恭喜你，第一步完成了。&lt;/p&gt;

&lt;p&gt;系统中的所有设备只能按照iommu group为单位分配给宿主机或者虚拟机。因此如果你看到两个不相关的设备在一个group里，那也没招，你只能给这俩设备一起丢虚拟机里去。&lt;/p&gt;

&lt;h3 id=&quot;在宿主系统中预留iommu-group&quot;&gt;在宿主系统中预留iommu group&lt;/h3&gt;

&lt;p&gt;很可惜的是，Linux系统并不支持预留iommu group。&lt;/p&gt;

&lt;p&gt;iommu group是硬件实现上的分组。在Linux的实现中，只认得各个在总线上的硬件，并挨个挨个的启动起来。&lt;/p&gt;

&lt;p&gt;我们要做的就是阻止Linux内核在启动时初始化某些组里的硬件。&lt;/p&gt;

&lt;p&gt;这里有一点需要非常注意的是，如果我们需要预留某个iommu组，我们必须完整的预留iommu组里的所有硬件。
如果我们的预留是不完整的，当我们把iommu组分配给虚拟机后，宿主系统会直接IO总线冲突，连重启的机会都没有。&lt;/p&gt;

&lt;h4 id=&quot;寻找我们需要预留的iommu-group&quot;&gt;寻找我们需要预留的iommu group&lt;/h4&gt;

&lt;p&gt;首先我们需要找到我们需要预留的iommu group的id：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ lspci | grep -i nvidia
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;（用amd卡的同学自行修改啊，后面不累述了，太累了）&lt;/p&gt;

&lt;p&gt;在我的电脑上，上面命令的输出如下：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;17:00.0 VGA compatible controller: NVIDIA Corporation GK106 [GeForce GTX 650 Ti] (rev a1)
17:00.1 Audio device: NVIDIA Corporation GK106 HDMI Audio Controller (rev a1)
65:00.0 VGA compatible controller: NVIDIA Corporation Device 2184 (rev a1)
65:00.1 Audio device: NVIDIA Corporation Device 1aeb (rev a1)
65:00.2 USB controller: NVIDIA Corporation Device 1aec (rev a1)
65:00.3 Serial bus controller [0c80]: NVIDIA Corporation Device 1aed (rev a1)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;可以看出我的电脑上有两张显卡，一张是GTX 650 Ti，另一张是NVIDIA Corporation Device 2184。
Google搜索&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NVIDIA Corporation Device 2184&lt;/code&gt;可知这是一张1660。&lt;/p&gt;

&lt;p&gt;很有意思的是，我的1660显卡竟然带了一个USB控制器？？？？就是这个控制器给我挖了个好大坑。
相当好奇的研究了一下Nvidia为什么要在显卡上带USB控制器，发现DP 1.2开始竟然是可以传输USB信号的。
坑爹的是，只有标准，没有产品，囧。&lt;/p&gt;

&lt;p&gt;我们需要把GTX 1660预留给虚拟机，因此我们记下id：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;65:00.0&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;如果你需要预留更多的东西，也都记下来，例如你觉得显卡带的声卡也是必要的。
（实际上，无论这里记不记声卡，最后还是得给声卡带上，因为65:00.1和65:00.0在同一个iommu group）&lt;/p&gt;

&lt;p&gt;接下来我们需要找出我们的设备所在的iommu group：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ dmesg | grep iommu | grep 65:00.0
[    1.210839] pci 0000:65:00.0: Adding to iommu group 34
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这说明我们需要预留的iommu group是34。&lt;/p&gt;

&lt;h4 id=&quot;找出iommu-group中的所有设备&quot;&gt;找出iommu group中的所有设备&lt;/h4&gt;

&lt;p&gt;这里再次强调，一个iommu group可能带有很多设备，我们只能按照iommu group为单位分配设备给虚拟机。&lt;/p&gt;

&lt;p&gt;在上面我们知道，我们需要预留的iommu group是34，那么我们继续：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ dmesg | grep &quot;iommu group 34&quot;
[    1.210839] pci 0000:65:00.0: Adding to iommu group 34
[    1.210874] pci 0000:65:00.1: Adding to iommu group 34
[    1.210907] pci 0000:65:00.2: Adding to iommu group 34
[    1.210939] pci 0000:65:00.3: Adding to iommu group 34
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;因此我们知道，我们需要把65:00.0 ~ 65:00.3全部给预留掉。&lt;/p&gt;

&lt;p&gt;然后我们需要找出65:00.0 ~ 65:00.3 PCI总线上的设备的id：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ lspci -nn | grep 65:00
65:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2184] (rev a1)
65:00.1 Audio device [0403]: NVIDIA Corporation Device [10de:1aeb] (rev a1)
65:00.2 USB controller [0c03]: NVIDIA Corporation Device [10de:1aec] (rev a1)
65:00.3 Serial bus controller [0c80]: NVIDIA Corporation Device [10de:1aed] (rev a1)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;注意右侧中括号中的id，因此我们需要预留的设备id列表为：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;10de:2184,10de:1aeb,10de:1aec,10de:1aed
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;在linux系统中预留设备&quot;&gt;在Linux系统中预留设备&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;2020-04-07更新：最新Ubuntu 20.04已经将vfio-pci模块编译进默认内核，推荐直接装Ubuntu 20.04，能避免一票子麻烦。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这一步主要有两个方案，一个是老一点的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pci-stub&lt;/code&gt;，另一个是新一点的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfio-pci&lt;/code&gt;，至于这两个的区别可以看这里：
https://unix.stackexchange.com/questions/328422/pci-stub-vs-vfio-pci&lt;/p&gt;

&lt;p&gt;如果不想折腾的话，推荐直接上&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pci-stub&lt;/code&gt;，动手能力强，喜欢折腾的，可以考虑&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfio&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pci-stub&lt;/code&gt;方案&lt;/p&gt;

    &lt;p&gt;我个人倾向于推荐&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pci-stub&lt;/code&gt;方案，因为大部分发行版都直接带了，不想要自己动手。
用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pci-stub&lt;/code&gt;预留设备非常简单，只需要在kernel启动参数中再加一个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pci-stub.ids=&lt;/code&gt;即可。
同样，我们打开&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/default/grub&lt;/code&gt;，并且修改&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GRUB_CMDLINE_LINUX_DEFAULT&lt;/code&gt;为：&lt;/p&gt;

    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GRUB_CMDLINE_LINUX_DEFAULT=&quot;quiet splash intel_iommu=on pci-stub.ids=10de:2184,10de:1aeb,10de:1aec,10de:1aed&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;

    &lt;p&gt;注意ids=后面就是我们前面记录下来的设备id列表
修改完了后，记得要&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo update-grub&lt;/code&gt;和重启电脑&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfio-pci&lt;/code&gt;方案&lt;/p&gt;

    &lt;p&gt;我这里并不想仔细的描述&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfio-pci&lt;/code&gt;方案，因为实在是太tmd的折腾了。我在这里只描述&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfio-pci&lt;/code&gt;方案可能遇到的问题，以及怎么解决，并不列具体怎么处理了。&lt;/p&gt;

    &lt;p&gt;Ubuntu在默认情况下，把vfio-pci编译成了一个内核模块，而不是编译进内核。&lt;/p&gt;

    &lt;p&gt;Linux现有的驱动机制是每个驱动在初始化时，自行去寻找没有初始化的，自己关注的硬件。因此驱动的初始化顺序在我们的需求里非常的关键。
例如，我们的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfio-pci&lt;/code&gt;就必须在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia&lt;/code&gt;之前初始化，否则话&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia&lt;/code&gt;就会把两张显卡都启用了，然后我们的预留就失败了。&lt;/p&gt;

    &lt;p&gt;Linux内核总是先初始化所有内建的驱动，然后才逐个初始化外部模块。
在外部模块中，我们可以通过softdep来增加模块间的依赖关键：
例如我们可以把&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfio-pci&lt;/code&gt;列为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia&lt;/code&gt;的依赖，那么&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfio-pci&lt;/code&gt;总会被在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia&lt;/code&gt;之前初始化，而确保能抢占到硬件。&lt;/p&gt;

    &lt;p&gt;在实施&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfio-pci&lt;/code&gt;方案之前，我们必须先检查我们需要预留的所有硬件目前是被什么启动使用的。&lt;/p&gt;

    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ lspci -nnv
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;

    &lt;p&gt;上面的命令会输出大量的内容，节选一段如下：&lt;/p&gt;

    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;17:00.0 VGA compatible controller: NVIDIA Corporation GK106 [GeForce GTX 650 Ti] (rev a1) (prog-if 00 [VGA controller])
        Subsystem: NVIDIA Corporation GK106 [GeForce GTX 650 Ti]
        Flags: bus master, fast devsel, latency 0, IRQ 44, NUMA node 0
        Memory at b4000000 (32-bit, non-prefetchable) [size=16M]
        Memory at a8000000 (64-bit, prefetchable) [size=128M]
        Memory at b0000000 (64-bit, prefetchable) [size=32M]
        I/O ports at 7000 [size=128]
        [virtual] Expansion ROM at 000c0000 [disabled] [size=128K]
        Capabilities: &amp;lt;access denied&amp;gt;
        Kernel driver in use: nvidia
        Kernel modules: nvidiafb, nouveau, nvidia_drm, nvidia

17:00.1 Audio device: NVIDIA Corporation GK106 HDMI Audio Controller (rev a1)
        Subsystem: NVIDIA Corporation GK106 HDMI Audio Controller
        Flags: bus master, fast devsel, latency 0, IRQ 61, NUMA node 0
        Memory at b5080000 (32-bit, non-prefetchable) [size=16K]
        Capabilities: &amp;lt;access denied&amp;gt;
        Kernel driver in use: snd_hda_intel
        Kernel modules: snd_hda_intel
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;

    &lt;p&gt;我们可以在上面的输出中找到Kernel driver in use，也就是说：17:00.0目前被&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia&lt;/code&gt;驱动使用，17:00.1目前被&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;snd_hda_intel&lt;/code&gt;使用&lt;/p&gt;

    &lt;p&gt;如果我们需要预留的所有设备的驱动都是外部模块，那么我们可以通过softdep强制修正驱动的加载顺序。以确保&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfio-pci&lt;/code&gt;在这些驱动加载之前先加载。
如果我们需要预留的设备中，有某个设备是内核内建驱动驱动的，那我们也有两个选择，要么把&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfio-pci&lt;/code&gt;编译成内建驱动，要么把相应的驱动从内核剥离出来成为模块。&lt;/p&gt;

    &lt;p&gt;很不幸，我遇到的就是第二种情况。由于我的显卡上带了一usb控制器，而usb控制器的驱动xhci_hcd是内建在内核的。&lt;/p&gt;

    &lt;p&gt;我最后选择的方式就是把&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfio-pci&lt;/code&gt;编译进内核。&lt;/p&gt;

    &lt;p&gt;如果选择把&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfio-pci&lt;/code&gt;编译进内核，那么剩下的事情就非常的简单了，和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pci-stub&lt;/code&gt;方案类似，只需要在内核启动参数中加入：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfio-pci.ids=&lt;/code&gt;参数即可。&lt;/p&gt;

    &lt;p&gt;如果还是选择从模块加载&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfio-pci&lt;/code&gt;，那么&lt;/p&gt;

    &lt;ul&gt;
      &lt;li&gt;首先我们需要修改&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/initramfs-tools/modules&lt;/code&gt;，vfio相关的模块全列进去。以及vfio的参数也在这里&lt;/li&gt;
      &lt;li&gt;然后在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/modprobe.d&lt;/code&gt;目录增加一些文件，声明模块间的依赖关系&lt;/li&gt;
      &lt;li&gt;再然后用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sudo update-initramfs -u&lt;/code&gt;更新系统的initramfs&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;完成预留后，我们需要确保预留的设备被正确的预留了&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ lspci -nnv | grep -E &quot;(^\S|Kernel driver in use)&quot; | grep 65:00 -A 1
65:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2184] (rev a1) (prog-if 00 [VGA controller])
        Kernel driver in use: vfio-pci
65:00.1 Audio device [0403]: NVIDIA Corporation Device [10de:1aeb] (rev a1)
        Kernel driver in use: vfio-pci
65:00.2 USB controller [0c03]: NVIDIA Corporation Device [10de:1aec] (rev a1) (prog-if 30 [XHCI])
        Kernel driver in use: vfio-pci
65:00.3 Serial bus controller [0c80]: NVIDIA Corporation Device [10de:1aed] (rev a1)
        Kernel driver in use: vfio-pci
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;注意在上面的输出中，有一个Kernel driver in use， 确保所有需要预留的设备都是被&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfio-pci&lt;/code&gt;或者&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pci-stub&lt;/code&gt;使用。
如果不是的话，请反复检查到底哪里出错了。如果这一步不正确，请不要继续尝试下面的动作，避免损坏硬件。&lt;/p&gt;

&lt;h3 id=&quot;创建kvm虚拟机并将iommu-group分配给虚拟机&quot;&gt;创建kvm虚拟机，并将iommu group分配给虚拟机&lt;/h3&gt;

&lt;p&gt;如果你到了这里了，恭喜恭喜，你已经顺利通关90%了。&lt;/p&gt;

&lt;p&gt;下一步我们需要安装kvm虚拟机，kvm虚拟机和平时用的虚拟机其实没有什么大区别，尤其是用图形节目的话。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ sudo apt-get install qemu-kvm libvirt-clients libvirt-daemon-system bridge-utils virt-manager ovmf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Ubuntu 18.04的qemu默认配置有个bug，貌似selinux起不来。我们需要先给这个处理一下：（其他系统不需要）
打开&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/libvirt/qemu.conf&lt;/code&gt;，找到security_driver所在行，改为：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;security_driver = &quot;none&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后保存，重启libvirt服务：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ sudo systemctl restart libvirtd.service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在这节里面我不打算去解释例如如何在kvm设置网桥之类的，或者如何改进kvm虚拟机IO性能之类的话题。
因为这和我们这次的主题并没有什么关系。GPU直通搞定了后，大家可以单独慢慢研究琢磨。
我在上面的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apt-get&lt;/code&gt;中安装网桥和ovmf，如果大家不需要，可以直接删掉。&lt;/p&gt;

&lt;p&gt;安装完后，可以直接启用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;virt-manager&lt;/code&gt;，这是一个libvirt的图形管理界面。
起来后，大家直奔创建虚拟机：
&lt;img src=&quot;/assets/posts/kvm-gpu-passthrough/1.png&quot; alt=&quot;1&quot; /&gt;&lt;/p&gt;

&lt;p&gt;整个创建过程和普通虚拟机并没有什么区别，就是分配磁盘，设置资源。（除了界面真的不好用）
在创建虚拟机的最后一步，建议勾选&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Customize configuration before install&lt;/code&gt;
&lt;img src=&quot;/assets/posts/kvm-gpu-passthrough/2.png&quot; alt=&quot;2&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在CPU型号的选项框中填入：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;host-passthrough&lt;/code&gt;，（这一步在我这里好像不是必须的，但是考虑到可能涉及到cpu差异，我还是写上了）
然后点Begin Installation
&lt;img src=&quot;/assets/posts/kvm-gpu-passthrough/3.png&quot; alt=&quot;3&quot; /&gt;&lt;/p&gt;

&lt;p&gt;OK，接下来就是安装Windows的环节了，大家该怎么装怎么装，该怎么调怎么调。安装完Windows后，关机虚拟机，给虚拟机增加PCI-E。&lt;/p&gt;

&lt;p&gt;点击虚拟机上工具栏的第二个按钮进入配置界面，点击左下角的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Add Hardware&lt;/code&gt;，选择&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PCI Host Device&lt;/code&gt;，选择我们要添加的PCI设备。
重复上面的动作多次，直到所有的设备都被添加完成。
注意，必须一次性把整个iommu group的设备全部加入到虚拟机，否则虚拟机会无法启动。
同样，被加入的设备必须是已经被操作系统预留的，否则会直接死机，还可能损坏硬件。
&lt;img src=&quot;/assets/posts/kvm-gpu-passthrough/4.png&quot; alt=&quot;4&quot; /&gt;&lt;/p&gt;

&lt;p&gt;最后点保存，找个备用显示器，插入到虚拟机的显卡，开机虚拟机。
开机完后，检查Windows驱动管理器，看看有没有识别到新的设备。
如果你用的是AMD显卡的话，可能到这步就结束了。
如果你用的是Nvidia显卡的话，到这步看到的是不可识别的PCI设备，或者是43错误。我们需要在下面的步骤修正这个问题。&lt;/p&gt;

&lt;h3 id=&quot;阻止nvidia驱动发现虚拟机&quot;&gt;阻止nvidia驱动发现虚拟机&lt;/h3&gt;

&lt;p&gt;nvidia在他自己带的驱动里，会默认阻止驱动在虚拟机上工作。
我不知道nvidia为什么要这么做，可能是为了避免问题？也有可能是nvidia有什么开发模式之类的。
修正这个问题很容易。&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;关机虚拟机&lt;/li&gt;
  &lt;li&gt;打开命令行，运行&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EDITOR=gedit virsh edit your-windows-vm-name&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;在打开的编辑器中，添加截图中的几行字
&lt;img src=&quot;/assets/posts/kvm-gpu-passthrough/5.png&quot; alt=&quot;5&quot; /&gt;&lt;/li&gt;
  &lt;li&gt;保存退出&lt;/li&gt;
  &lt;li&gt;重启虚拟机&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;设置鼠标键盘显示器&quot;&gt;设置鼠标键盘显示器&lt;/h3&gt;

&lt;p&gt;完成了上面的动作之后，整个gpu直通就搞定了。
但是接下来鼠标键盘显示器的问题非常的蛋疼。你有两个选择：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;买两套鼠标键盘显示器&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;把第二个显示器插到虚拟机挂载的显卡上
把第二套鼠标键盘接到usb，然后usb直通到虚拟机上&lt;/p&gt;

&lt;p&gt;如果用这套方案的话，相当与有了两台电脑，emmmm….&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;搞个kvm，俺一下切换鼠标键盘显示器&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;emmmm… 这不也相当于是两台电脑么。&lt;/p&gt;

&lt;p&gt;但是用kvm这个方案的话其实还是有点特殊的。如果你接的两个键盘，那么kvm把其中一个键盘分配给虚拟机。
但是如果你用一个键盘+kvm。kvm切换的时候，其实只是相当与键盘鼠标换了个usb插口啊。键盘还是那个键盘，鼠标还是那个鼠标。
要么全分配给了宿主机，要么全分配给了虚拟机，这不就囧了么。&lt;/p&gt;

&lt;p&gt;幸运的是，我主板上有2个usb控制器，分别在iommu group 4和13
于是重复一下上面的动作，我把第二个usb控制器也划给了虚拟机。
但是我觉得像我这种情况的可能应该不在多数。一个简单的解决方案是，再买个PCI-E的usb扩展卡，不到100大洋，这样也是一个独立的iommu group。&lt;/p&gt;

&lt;h3 id=&quot;修正spice模式下的鼠标问题&quot;&gt;修正spice模式下的鼠标问题&lt;/h3&gt;

&lt;p&gt;有时候我们只是想开个小窗口，看看虚拟机里到底发生了啥。而并不想把kvm切换过去看。
这个时候，一个非常好的主意就是留一个kvm的spice显示器，用来观察虚拟机。
如果用的kvm还是比较低端的硬件kvm（不支持虚拟显示器）的话，还可以把显卡直通的输出显示器设置为扩展显示器，在Windows中设置仅显示扩展显示。&lt;/p&gt;

&lt;p&gt;但是kvm的spice有点小问题，一旦你把显示器切换到外接显示器，再切换回来后，鼠标就不能正常工作了。
我们可以在kvm的配置里强制spice的鼠标为server模式来绕开这个问题：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;关闭虚拟机&lt;/li&gt;
  &lt;li&gt;打开命令行，运行&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EDITOR=gedit virsh edit your-windows-vm-name&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;在打开的编辑器中，添加截图中的内容&lt;br /&gt;
&lt;img src=&quot;/assets/posts/kvm-gpu-passthrough/6.png&quot; alt=&quot;6&quot; /&gt;&lt;/li&gt;
  &lt;li&gt;保存退出&lt;/li&gt;
  &lt;li&gt;重启虚拟机&lt;/li&gt;
&lt;/ol&gt;</content><author><name>Chizhong Jin</name><email>pjincz@gmail.com</email></author><category term="zh" /><summary type="html">最近工作的事情有点烦躁，就开始打酱油了。折腾了好久，可算把家里的nas和游戏机合为一体了。 这其中最蛋疼的步骤可算是要在Linux虚拟机上跑游戏了。在无数次死机重启之后，可算把这个问题搞定的。 网上GPU虚拟机直通的教程不少，但是都有一个问题，就是只告诉你怎么一步一步做，然后启用了一个又一个Linux模块，但是往往搞到最后一启动虚拟机就整个电脑死机了（囧，对的，整个电脑死机，不是虚拟机死机）。 踩了n天的坑后，总算把GPU直通的所有坑都踩了一遍。也都看了一遍。这里记录下来，有兴趣的朋友一起研究研究吧。 整个虚拟机GPU直通大约有那么几个步骤（缺1不可），我的平台是Intel + Nvidia，因此用AMD的朋友可能会有所不同。但是整体思路应该是一致的。 确认硬件支持 主要是CPU和主板需要支持VT-d技术，如果是AMD平台的话，应该是AMD-Vi支持。 具体行不行，直接Google之。 买买买 如果你只有一张显卡的话，GPU直通显然是没法玩的，显卡分配给了虚拟机，宿主机怎么办？ 当然你可以搞两张一样的卡，我个人的话直接搞了张二手GTX 650，便宜实惠。反正宿主机对显卡也没啥要求。 理论上来说A卡N卡混合应该无所谓，但我没试过。 理论上来说，如果你有集成显卡，有些主板有选项，可以默认从集成显卡启动，这样的话你可以省掉一张显卡。（很遗憾i9连核显都没有） 省掉一张显卡主要不是为了省钱啊，我现在的机箱已经重到搬不动了，两张显卡，3块大硬盘，2个SSD，还有水冷啥的，塞的满满当当的，囧。 另外推荐入手一个kvm切换器，等显卡直通搞定了，你就相当与有了两台电脑了，没个kvm不得烦死？ 另外，如果主板的所有USB口都在一个iommu group的话，推荐再入手个PCI-E的usb扩展卡。后面会仔细解释这个事情。 在BIOS中调整相关参数 首先肯定是要启用VT-d的啦，（AMD的话就是AMD-Vi） 然后还有一个重要选项是默认从哪张显卡输出。可惜不是所有BIOS都支持这个选项，我的老板子就不支持，前端时间坏了（真假），被我顺利淘汰了。 如果有这个选项的话，就可以把好显卡插在主PCI-E槽，然后默认从差的显卡启动。如果没有的话，想开点，交换一下吧。 在宿主系统中启用iommu iommu是GPU虚拟机直通的核心。 虽然我们总是在讲GPU直通，其实本质上来说，通过VT-d技术，各种乱七八糟的东西都可以丢到虚拟机里面去，让虚拟机直接访问。 iommu负责把主板上的设备分组，分成一个一个group。每个group都可以单独的启用或者停用。 在没启用iommu之前，宿主系统会占用全部的硬件。在启用iommu之后，宿主系统可以有选择的预留一些硬件资源，把这些硬件资源分配给虚拟机。 在宿主系统中预留iommu group 无论有没有启用iommu，Linux默认会扫描系统中的所有硬件，并尝试给每个硬件分配驱动，并初始化。 这个事情的直接后果就是，系统启动完了后，iommu倒是启用了，但是没有闲着的分组？？？ 所以我们需要在Linux内核启动足够早的阶段把硬件预留下来。 这个步骤是整个事情中最麻烦的，也最有可能遇到问题的。 创建kvm虚拟机，并将iommu group分配给虚拟机 这个简单 给虚拟机安装系统 emmm…. 阻止nvidia驱动发现虚拟机 这个步骤我是真的真的想吐槽，所有一切都就绪了之后，系统起来被锁定在800x600的分辨率，驱动提示错误35。 刚开始我始终以为是自己哪里做错了。结果上网一搜发现，竟然是nvidia的驱动直接去侦测自己是不是在虚拟机里跑的。如果是的话就直接报错。 修正这个问题不难，kvm自身有特性可以隐藏虚拟机，直接打开就能解决这个问题。但是这破问题坑了我好几个小时。 安装steam, 安装游戏, 加我steam好友 [狗头] 总体来说就这么多步骤。其中大部分的步骤都不难，最有挑战的部分应该是预留iommu group。因为每个人的硬件不一样，遇到的情况也不一样。 本文不覆盖买买买，因为不打广告。 本文也不覆盖BIOS设置，因为我这非主流主板，覆盖了大家也没兴趣啊。 我们直接从如何启用iommu开始，一步一步的讲解，解完一步确认一步。 本文使用的操作系统为Ubuntu 18.04，其实系统影响不大，主要还是内核和硬件。但是不少步骤是系统相关的，例如如何修改grub启动参数等，如果在其他系统上实施，需要做相应的动作。 在宿主系统中启用iommu 在宿主系统中启用iommu很简单，就是给Linux内核一个额外的启动参数。 对Ubuntu来说，我们只需要修改/etc/default/grub配置文件即可。 打开/etc/default/grub，找到GRUB_CMDLINE_LINUX_DEFAULT，默认情况下这行大概是这样的： GRUB_CMDLINE_LINUX_DEFAULT=&quot;quiet splash&quot; 我们把这行改为： GRUB_CMDLINE_LINUX_DEFAULT=&quot;quiet splash intel_iommu=on&quot; 如果是AMD的话，据说是加amd_iommu=on iommu=pt kvm_amd.npt=1 kvm_amd.avic=1，没实验过，请自行尝试。 接着我们需要更新grub： $ sudo update-grub -u 然后重启电脑，重启完后我们需要检查iommu有没有被正确启用： $ dmesg | grep -i iommu 如果你在输出中看到很多pci xxxx:xx:xx.x: Adding to iommu group xx，那么恭喜你，第一步完成了。 系统中的所有设备只能按照iommu group为单位分配给宿主机或者虚拟机。因此如果你看到两个不相关的设备在一个group里，那也没招，你只能给这俩设备一起丢虚拟机里去。 在宿主系统中预留iommu group 很可惜的是，Linux系统并不支持预留iommu group。 iommu group是硬件实现上的分组。在Linux的实现中，只认得各个在总线上的硬件，并挨个挨个的启动起来。 我们要做的就是阻止Linux内核在启动时初始化某些组里的硬件。 这里有一点需要非常注意的是，如果我们需要预留某个iommu组，我们必须完整的预留iommu组里的所有硬件。 如果我们的预留是不完整的，当我们把iommu组分配给虚拟机后，宿主系统会直接IO总线冲突，连重启的机会都没有。 寻找我们需要预留的iommu group 首先我们需要找到我们需要预留的iommu group的id： $ lspci | grep -i nvidia （用amd卡的同学自行修改啊，后面不累述了，太累了） 在我的电脑上，上面命令的输出如下： 17:00.0 VGA compatible controller: NVIDIA Corporation GK106 [GeForce GTX 650 Ti] (rev a1) 17:00.1 Audio device: NVIDIA Corporation GK106 HDMI Audio Controller (rev a1) 65:00.0 VGA compatible controller: NVIDIA Corporation Device 2184 (rev a1) 65:00.1 Audio device: NVIDIA Corporation Device 1aeb (rev a1) 65:00.2 USB controller: NVIDIA Corporation Device 1aec (rev a1) 65:00.3 Serial bus controller [0c80]: NVIDIA Corporation Device 1aed (rev a1) 可以看出我的电脑上有两张显卡，一张是GTX 650 Ti，另一张是NVIDIA Corporation Device 2184。 Google搜索NVIDIA Corporation Device 2184可知这是一张1660。 很有意思的是，我的1660显卡竟然带了一个USB控制器？？？？就是这个控制器给我挖了个好大坑。 相当好奇的研究了一下Nvidia为什么要在显卡上带USB控制器，发现DP 1.2开始竟然是可以传输USB信号的。 坑爹的是，只有标准，没有产品，囧。 我们需要把GTX 1660预留给虚拟机，因此我们记下id：65:00.0。 如果你需要预留更多的东西，也都记下来，例如你觉得显卡带的声卡也是必要的。 （实际上，无论这里记不记声卡，最后还是得给声卡带上，因为65:00.1和65:00.0在同一个iommu group） 接下来我们需要找出我们的设备所在的iommu group： $ dmesg | grep iommu | grep 65:00.0 [ 1.210839] pci 0000:65:00.0: Adding to iommu group 34 这说明我们需要预留的iommu group是34。 找出iommu group中的所有设备 这里再次强调，一个iommu group可能带有很多设备，我们只能按照iommu group为单位分配设备给虚拟机。 在上面我们知道，我们需要预留的iommu group是34，那么我们继续： $ dmesg | grep &quot;iommu group 34&quot; [ 1.210839] pci 0000:65:00.0: Adding to iommu group 34 [ 1.210874] pci 0000:65:00.1: Adding to iommu group 34 [ 1.210907] pci 0000:65:00.2: Adding to iommu group 34 [ 1.210939] pci 0000:65:00.3: Adding to iommu group 34 因此我们知道，我们需要把65:00.0 ~ 65:00.3全部给预留掉。 然后我们需要找出65:00.0 ~ 65:00.3 PCI总线上的设备的id： $ lspci -nn | grep 65:00 65:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2184] (rev a1) 65:00.1 Audio device [0403]: NVIDIA Corporation Device [10de:1aeb] (rev a1) 65:00.2 USB controller [0c03]: NVIDIA Corporation Device [10de:1aec] (rev a1) 65:00.3 Serial bus controller [0c80]: NVIDIA Corporation Device [10de:1aed] (rev a1) 注意右侧中括号中的id，因此我们需要预留的设备id列表为： 10de:2184,10de:1aeb,10de:1aec,10de:1aed 在Linux系统中预留设备 2020-04-07更新：最新Ubuntu 20.04已经将vfio-pci模块编译进默认内核，推荐直接装Ubuntu 20.04，能避免一票子麻烦。 这一步主要有两个方案，一个是老一点的pci-stub，另一个是新一点的vfio-pci，至于这两个的区别可以看这里： https://unix.stackexchange.com/questions/328422/pci-stub-vs-vfio-pci 如果不想折腾的话，推荐直接上pci-stub，动手能力强，喜欢折腾的，可以考虑vfio. pci-stub方案 我个人倾向于推荐pci-stub方案，因为大部分发行版都直接带了，不想要自己动手。 用pci-stub预留设备非常简单，只需要在kernel启动参数中再加一个pci-stub.ids=即可。 同样，我们打开/etc/default/grub，并且修改GRUB_CMDLINE_LINUX_DEFAULT为： GRUB_CMDLINE_LINUX_DEFAULT=&quot;quiet splash intel_iommu=on pci-stub.ids=10de:2184,10de:1aeb,10de:1aec,10de:1aed&quot; 注意ids=后面就是我们前面记录下来的设备id列表 修改完了后，记得要sudo update-grub和重启电脑 vfio-pci方案 我这里并不想仔细的描述vfio-pci方案，因为实在是太tmd的折腾了。我在这里只描述vfio-pci方案可能遇到的问题，以及怎么解决，并不列具体怎么处理了。 Ubuntu在默认情况下，把vfio-pci编译成了一个内核模块，而不是编译进内核。 Linux现有的驱动机制是每个驱动在初始化时，自行去寻找没有初始化的，自己关注的硬件。因此驱动的初始化顺序在我们的需求里非常的关键。 例如，我们的vfio-pci就必须在nvidia之前初始化，否则话nvidia就会把两张显卡都启用了，然后我们的预留就失败了。 Linux内核总是先初始化所有内建的驱动，然后才逐个初始化外部模块。 在外部模块中，我们可以通过softdep来增加模块间的依赖关键： 例如我们可以把vfio-pci列为nvidia的依赖，那么vfio-pci总会被在nvidia之前初始化，而确保能抢占到硬件。 在实施vfio-pci方案之前，我们必须先检查我们需要预留的所有硬件目前是被什么启动使用的。 $ lspci -nnv 上面的命令会输出大量的内容，节选一段如下： 17:00.0 VGA compatible controller: NVIDIA Corporation GK106 [GeForce GTX 650 Ti] (rev a1) (prog-if 00 [VGA controller]) Subsystem: NVIDIA Corporation GK106 [GeForce GTX 650 Ti] Flags: bus master, fast devsel, latency 0, IRQ 44, NUMA node 0 Memory at b4000000 (32-bit, non-prefetchable) [size=16M] Memory at a8000000 (64-bit, prefetchable) [size=128M] Memory at b0000000 (64-bit, prefetchable) [size=32M] I/O ports at 7000 [size=128] [virtual] Expansion ROM at 000c0000 [disabled] [size=128K] Capabilities: &amp;lt;access denied&amp;gt; Kernel driver in use: nvidia Kernel modules: nvidiafb, nouveau, nvidia_drm, nvidia 17:00.1 Audio device: NVIDIA Corporation GK106 HDMI Audio Controller (rev a1) Subsystem: NVIDIA Corporation GK106 HDMI Audio Controller Flags: bus master, fast devsel, latency 0, IRQ 61, NUMA node 0 Memory at b5080000 (32-bit, non-prefetchable) [size=16K] Capabilities: &amp;lt;access denied&amp;gt; Kernel driver in use: snd_hda_intel Kernel modules: snd_hda_intel 我们可以在上面的输出中找到Kernel driver in use，也就是说：17:00.0目前被nvidia驱动使用，17:00.1目前被snd_hda_intel使用 如果我们需要预留的所有设备的驱动都是外部模块，那么我们可以通过softdep强制修正驱动的加载顺序。以确保vfio-pci在这些驱动加载之前先加载。 如果我们需要预留的设备中，有某个设备是内核内建驱动驱动的，那我们也有两个选择，要么把vfio-pci编译成内建驱动，要么把相应的驱动从内核剥离出来成为模块。 很不幸，我遇到的就是第二种情况。由于我的显卡上带了一usb控制器，而usb控制器的驱动xhci_hcd是内建在内核的。 我最后选择的方式就是把vfio-pci编译进内核。 如果选择把vfio-pci编译进内核，那么剩下的事情就非常的简单了，和pci-stub方案类似，只需要在内核启动参数中加入：vfio-pci.ids=参数即可。 如果还是选择从模块加载vfio-pci，那么 首先我们需要修改/etc/initramfs-tools/modules，vfio相关的模块全列进去。以及vfio的参数也在这里 然后在/etc/modprobe.d目录增加一些文件，声明模块间的依赖关系 再然后用sudo update-initramfs -u更新系统的initramfs 完成预留后，我们需要确保预留的设备被正确的预留了 $ lspci -nnv | grep -E &quot;(^\S|Kernel driver in use)&quot; | grep 65:00 -A 1 65:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2184] (rev a1) (prog-if 00 [VGA controller]) Kernel driver in use: vfio-pci 65:00.1 Audio device [0403]: NVIDIA Corporation Device [10de:1aeb] (rev a1) Kernel driver in use: vfio-pci 65:00.2 USB controller [0c03]: NVIDIA Corporation Device [10de:1aec] (rev a1) (prog-if 30 [XHCI]) Kernel driver in use: vfio-pci 65:00.3 Serial bus controller [0c80]: NVIDIA Corporation Device [10de:1aed] (rev a1) Kernel driver in use: vfio-pci 注意在上面的输出中，有一个Kernel driver in use， 确保所有需要预留的设备都是被vfio-pci或者pci-stub使用。 如果不是的话，请反复检查到底哪里出错了。如果这一步不正确，请不要继续尝试下面的动作，避免损坏硬件。 创建kvm虚拟机，并将iommu group分配给虚拟机 如果你到了这里了，恭喜恭喜，你已经顺利通关90%了。 下一步我们需要安装kvm虚拟机，kvm虚拟机和平时用的虚拟机其实没有什么大区别，尤其是用图形节目的话。 $ sudo apt-get install qemu-kvm libvirt-clients libvirt-daemon-system bridge-utils virt-manager ovmf Ubuntu 18.04的qemu默认配置有个bug，貌似selinux起不来。我们需要先给这个处理一下：（其他系统不需要） 打开/etc/libvirt/qemu.conf，找到security_driver所在行，改为： security_driver = &quot;none&quot; 然后保存，重启libvirt服务： $ sudo systemctl restart libvirtd.service 在这节里面我不打算去解释例如如何在kvm设置网桥之类的，或者如何改进kvm虚拟机IO性能之类的话题。 因为这和我们这次的主题并没有什么关系。GPU直通搞定了后，大家可以单独慢慢研究琢磨。 我在上面的apt-get中安装网桥和ovmf，如果大家不需要，可以直接删掉。 安装完后，可以直接启用virt-manager，这是一个libvirt的图形管理界面。 起来后，大家直奔创建虚拟机： 整个创建过程和普通虚拟机并没有什么区别，就是分配磁盘，设置资源。（除了界面真的不好用） 在创建虚拟机的最后一步，建议勾选Customize configuration before install 在CPU型号的选项框中填入：host-passthrough，（这一步在我这里好像不是必须的，但是考虑到可能涉及到cpu差异，我还是写上了） 然后点Begin Installation OK，接下来就是安装Windows的环节了，大家该怎么装怎么装，该怎么调怎么调。安装完Windows后，关机虚拟机，给虚拟机增加PCI-E。 点击虚拟机上工具栏的第二个按钮进入配置界面，点击左下角的Add Hardware，选择PCI Host Device，选择我们要添加的PCI设备。 重复上面的动作多次，直到所有的设备都被添加完成。 注意，必须一次性把整个iommu group的设备全部加入到虚拟机，否则虚拟机会无法启动。 同样，被加入的设备必须是已经被操作系统预留的，否则会直接死机，还可能损坏硬件。 最后点保存，找个备用显示器，插入到虚拟机的显卡，开机虚拟机。 开机完后，检查Windows驱动管理器，看看有没有识别到新的设备。 如果你用的是AMD显卡的话，可能到这步就结束了。 如果你用的是Nvidia显卡的话，到这步看到的是不可识别的PCI设备，或者是43错误。我们需要在下面的步骤修正这个问题。 阻止nvidia驱动发现虚拟机 nvidia在他自己带的驱动里，会默认阻止驱动在虚拟机上工作。 我不知道nvidia为什么要这么做，可能是为了避免问题？也有可能是nvidia有什么开发模式之类的。 修正这个问题很容易。 关机虚拟机 打开命令行，运行EDITOR=gedit virsh edit your-windows-vm-name 在打开的编辑器中，添加截图中的几行字 保存退出 重启虚拟机 设置鼠标键盘显示器 完成了上面的动作之后，整个gpu直通就搞定了。 但是接下来鼠标键盘显示器的问题非常的蛋疼。你有两个选择： 买两套鼠标键盘显示器 把第二个显示器插到虚拟机挂载的显卡上 把第二套鼠标键盘接到usb，然后usb直通到虚拟机上 如果用这套方案的话，相当与有了两台电脑，emmmm…. 搞个kvm，俺一下切换鼠标键盘显示器 emmmm… 这不也相当于是两台电脑么。 但是用kvm这个方案的话其实还是有点特殊的。如果你接的两个键盘，那么kvm把其中一个键盘分配给虚拟机。 但是如果你用一个键盘+kvm。kvm切换的时候，其实只是相当与键盘鼠标换了个usb插口啊。键盘还是那个键盘，鼠标还是那个鼠标。 要么全分配给了宿主机，要么全分配给了虚拟机，这不就囧了么。 幸运的是，我主板上有2个usb控制器，分别在iommu group 4和13 于是重复一下上面的动作，我把第二个usb控制器也划给了虚拟机。 但是我觉得像我这种情况的可能应该不在多数。一个简单的解决方案是，再买个PCI-E的usb扩展卡，不到100大洋，这样也是一个独立的iommu group。 修正spice模式下的鼠标问题 有时候我们只是想开个小窗口，看看虚拟机里到底发生了啥。而并不想把kvm切换过去看。 这个时候，一个非常好的主意就是留一个kvm的spice显示器，用来观察虚拟机。 如果用的kvm还是比较低端的硬件kvm（不支持虚拟显示器）的话，还可以把显卡直通的输出显示器设置为扩展显示器，在Windows中设置仅显示扩展显示。 但是kvm的spice有点小问题，一旦你把显示器切换到外接显示器，再切换回来后，鼠标就不能正常工作了。 我们可以在kvm的配置里强制spice的鼠标为server模式来绕开这个问题： 关闭虚拟机 打开命令行，运行EDITOR=gedit virsh edit your-windows-vm-name 在打开的编辑器中，添加截图中的内容 保存退出 重启虚拟机</summary></entry><entry><title type="html">如何设计一个平衡树结构</title><link href="https://mechanical-consciousness.com/2019/01/21/how-to-design-a-balance-tree-zh.html" rel="alternate" type="text/html" title="如何设计一个平衡树结构" /><published>2019-01-21T00:00:00+08:00</published><updated>2019-01-21T00:00:00+08:00</updated><id>https://mechanical-consciousness.com/2019/01/21/how-to-design-a-balance-tree-zh</id><content type="html" xml:base="https://mechanical-consciousness.com/2019/01/21/how-to-design-a-balance-tree-zh.html">&lt;p&gt;(感谢”杀出一条蟹路“同学帮忙复核数学推理)&lt;/p&gt;

&lt;p&gt;昨天在群里和一些同学讨论算法，扯到平衡树。突然想写点东西，给同学们分享分享。但是我今天要分享的不是有多少种平衡树实现，而是如何设计一种平衡树。&lt;/p&gt;

&lt;p&gt;希望通过这个分享，能让同学们有一些基础的认识，一种数据结构/算法是如何设计出来的。&lt;/p&gt;

&lt;p&gt;而我们今天的话题，就是如何在树节点上，通过附加一个size来进行树平衡。&lt;/p&gt;

&lt;h2 id=&quot;0-开始之前&quot;&gt;0. 开始之前&lt;/h2&gt;

&lt;p&gt;当我写这篇文章的时候，我希望对算法有所了解的同学都能看懂。数据结构设计相对于算法来说，还是有那么一点点的偏难。有些概念可能大家都是第一次接触，因此我在这里先进行一些基础概念的讲解。&lt;/p&gt;

&lt;h3 id=&quot;约束&quot;&gt;约束&lt;/h3&gt;

&lt;p&gt;数据结构设计和一般性的算法有一个非常大的差异，就是：&lt;/p&gt;

&lt;p&gt;数据结构是一个在一段时间内存在，并持续维护的结构。而不是一次性运行结束的函数调用。&lt;/p&gt;

&lt;p&gt;这中间的最大差异是，假设我们现在定义一棵树是平衡的，那么树的高度&lt;script type=&quot;math/tex&quot;&gt;% &lt;![CDATA[
H &lt; 2 log_2(n+1) %]]&gt;&lt;/script&gt;。假设我们这时需要实现一个函数&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;isBalanceTree(root)&lt;/code&gt;，那么显然除了完整的遍历整棵外，我们并没有其他什么办法。显然这个时间复杂度是O(n)的。&lt;/p&gt;

&lt;p&gt;判断一个树是否是平衡的，这个都是O(n)的了，如果我们需要写一个函数&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;balanceTree&lt;/code&gt;去平衡一棵树，这个时间复杂度至少是O(n)。&lt;/p&gt;

&lt;p&gt;如果说我们写一棵普通的二叉树，然后每次插入/删除后，我们运行一次&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;balanceTree&lt;/code&gt;我们也可以得到一棵平衡树。但是这个明显没有多大意义。还不如弄个数组，每次先定位再插入，不但简单，还来的快。&lt;/p&gt;

&lt;p&gt;那我们如何来解决这个问题，中间的“秘密”就是约束。&lt;/p&gt;

&lt;p&gt;所谓约束就是：数据结构在“平时”应该满足什么限制。&lt;/p&gt;

&lt;p&gt;如果说一个数据结构，只要满足了某约束，就能带来某个我们想要的特性。而我们每次修改数据结构时，只需要很小的代价，就能保持这个约束。那么这就是我们想要的。&lt;/p&gt;

&lt;p&gt;当然，上面的讲的太抽象了，这里我们拿个红黑树举例。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/posts/how-to-design-a-balance-tree-zh/RedBlackTree.png&quot; alt=&quot;Rotate tree&quot; /&gt;&lt;/p&gt;

&lt;p&gt;红黑树在二叉树的基础上，引入了4条约束。&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;节点必须是红色或者黑色&lt;/li&gt;
  &lt;li&gt;根节点是黑色&lt;/li&gt;
  &lt;li&gt;红色节点不能和红色节点相邻&lt;/li&gt;
  &lt;li&gt;从根节点出发，到任意nil节点所路过的黑节点数量一致&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;假如说我们记从根节点出发，黑色节点数为d，树的最大高度为h。&lt;/p&gt;

&lt;p&gt;从约束4我们知道，d对于任意路径都一样。&lt;/p&gt;

&lt;p&gt;从约束1和3我们知道，&lt;script type=&quot;math/tex&quot;&gt;h \le 2d&lt;/script&gt;。&lt;/p&gt;

&lt;p&gt;另外我们知道，对于任意红黑树，&lt;script type=&quot;math/tex&quot;&gt;总节点数n \ge 黑节点数 \ge 2^d-1&lt;/script&gt;&lt;/p&gt;

&lt;p&gt;于是，我们知道：&lt;script type=&quot;math/tex&quot;&gt;h \le 2d \le 2log_2(n+1)&lt;/script&gt;&lt;/p&gt;

&lt;p&gt;因此，我们从红黑树的约定中，我们推出了我们想要的平衡。&lt;/p&gt;

&lt;p&gt;另外，红黑树对于插入/删除时，有一套完整的修正约束的方法，这里我们就不展开讲了，有兴趣的同学戳这里：&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E7%BA%A2%E9%BB%91%E6%A0%91&quot;&gt;维基百科-红黑树&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;注：上面的分析中，我们并没有用到约束2，约束2不是可选的，是红黑树上升的重要约束。&lt;/p&gt;

&lt;h3 id=&quot;分析&quot;&gt;分析&lt;/h3&gt;

&lt;p&gt;接着我们需要一个合适的衡量指标去衡量我们的目标是否达成。就像是我上面举的定义，稍作放松是完全可以用的。但是这里我还是想探讨一下如何定义一个良好的衡量，用作后续的分析。&lt;/p&gt;

&lt;p&gt;显然，“平衡树”是一棵树哈。我们这里更关心的是“平衡”。所谓的“平衡”就是：树中间的最深叶子节点的深度和最浅叶子节点的深度差的“不太大”。当然这个定义有点“不太数学”。&lt;/p&gt;

&lt;p&gt;考虑到一个h层深度的满树能容纳的节点数n为：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;n = 2^h - 1&lt;/script&gt;

&lt;p&gt;因此，对于一个n个节点的树，我们能获得的最小的高度为：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;h = \lceil log_2(n + 1) \rceil&lt;/script&gt;

&lt;p&gt;我们可以把平衡树定义为：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;h \le k * \lceil log_2(n + 1) \rceil&lt;/script&gt;

&lt;p&gt;其中k可以视为平衡系数。显然当k=1时，树不可能更“平衡”了，当k是常数时，我们访问树的任意节点的时间复杂度不超过&lt;script type=&quot;math/tex&quot;&gt;klog_2n&lt;/script&gt;，也就是&lt;script type=&quot;math/tex&quot;&gt;O(log_2n)&lt;/script&gt;。显然我们可以认为，这个时候树是平衡的。&lt;/p&gt;

&lt;p&gt;由于ceil函数在分析时会给我们带来麻烦，因此我们把上面的条件弱化为：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;h \le k * log_2n + b&lt;/script&gt;

&lt;p&gt;其中k和b都是常数。这个时候访问任意节点，我们的时间复杂度仍然不超过&lt;script type=&quot;math/tex&quot;&gt;log_2n&lt;/script&gt;。&lt;/p&gt;

&lt;p&gt;需要注意的是，上面的这些定义都是仅限于这篇文章（虽然你拿出去用问题也不大）。另外在实际设计设计过程中，不需要把这些东西精确的写出来，一般模糊的给自己一个感觉：访问节点的时间最糟情况下应该是对数时间，就够了。&lt;/p&gt;

&lt;p&gt;前面我们已经分析了红黑树是可以满足这个条件的。而且是一棵非常好的平衡树(k=2，实际上k&amp;lt;2因为我们进行了一个比较“大方”的缩放，具体数值有兴趣可以自行细算)。&lt;/p&gt;

&lt;p&gt;如果同学们有兴趣的话，还可以对&lt;a href=&quot;https://zh.wikipedia.org/wiki/AVL%E6%A0%91&quot;&gt;AVL树&lt;/a&gt;进行一下分析，AVL分析的过程中还可以出现很好玩的斐波那契数列哈哈哈。然后可以得到一个k大约等于1.44（随手算的，错了请无视）。可见AVL比红黑树平衡性要更强。&lt;/p&gt;

&lt;p&gt;当然，平衡性更强不代表数据结构更好，虽然更强的平衡可以在访问时获得更短的路径，但是更强的平衡性同时会导致插入删除过程中更多的失衡调整。因此具体那种情况更有，还需要更多的考量。当然已经超越本文探讨的范畴了。&lt;/p&gt;

&lt;h2 id=&quot;1-可行性分析&quot;&gt;1. 可行性分析&lt;/h2&gt;

&lt;p&gt;我们需要分析使用子树size进行平衡的可能性。这时我们首先引入我们的第一个约束：&lt;/p&gt;

&lt;p&gt;约束1：每个节点附带一个size = 所在节点以及所有子节点的个数&lt;/p&gt;

&lt;p&gt;这是一个蛮简单明了的约束，显然，维护这么一个约束并不难。每次插入时，我们只需要从插入位置一直到根节点，一路更新size即可。对于删除问题也差不多。&lt;/p&gt;

&lt;p&gt;当然，这个约束明显不足以建立平衡树，我们需要考虑在这个约束的基础上再加入更复杂的约束。这时我们需要考虑几个条件：&lt;/p&gt;

&lt;h3 id=&quot;通过某种对左右树size的约束对于任何情况树是可以建立起来的&quot;&gt;通过某种对左右树size的约束，对于任何情况，树是可以建立起来的。&lt;/h3&gt;

&lt;p&gt;第一个条件是明显需要满足的，否则树都不能建立起来，那还平衡个毛线。这条看起来很废话，但是实际上却不能不考虑。例如假设我们引入一个约束：&lt;/p&gt;

&lt;p&gt;&lt;script type=&quot;math/tex&quot;&gt;L \le 2R, R \le 2L&lt;/script&gt;。（其中&lt;script type=&quot;math/tex&quot;&gt;L, R&lt;/script&gt;分别为左右子树大小）&lt;/p&gt;

&lt;p&gt;这个乍一眼一看是一个蛮合理的约束，但是咱考虑一下，假设一棵树有两个节点&lt;script type=&quot;math/tex&quot;&gt;n=2&lt;/script&gt;，这棵树怎么办？&lt;/p&gt;

&lt;p&gt;显然我们需要1个根节点，那么我们把剩下的一个节点放入到左侧时，&lt;script type=&quot;math/tex&quot;&gt;L \le 2R&lt;/script&gt;就不成立了。反之，&lt;script type=&quot;math/tex&quot;&gt;% &lt;![CDATA[
R &lt;= 2L %]]&gt;&lt;/script&gt;就不成立了。因此，上述的约束不是一个可行的约束方案。&lt;/p&gt;

&lt;p&gt;但是考虑一下 &lt;script type=&quot;math/tex&quot;&gt;L \le 2R + 1, R \le 2L + 1&lt;/script&gt;，树就可以建立起来了。&lt;/p&gt;

&lt;h3 id=&quot;通过约束可以推导出平衡特性&quot;&gt;通过约束，可以推导出平衡特性&lt;/h3&gt;

&lt;p&gt;这显然又是一句废话，我们的目标是设计一颗平衡树，如果树不平衡我们还玩毛线啊。因此我们需要一个证明，证明只要我们的size一直符合我们的约束，树就是平衡的。&lt;/p&gt;

&lt;p&gt;在上面的例子中，我们用的是&lt;script type=&quot;math/tex&quot;&gt;L \le 2R + 1, R \le 2L + 1&lt;/script&gt;。但是我们在后面的研究中，系数2和1其实可能是会发生变化的，为了给后面留有余地，我们这里证明一下，如果一棵树任意节点的：&lt;script type=&quot;math/tex&quot;&gt;L \le kR + b, R \le kL + b&lt;/script&gt;，这颗树是平衡的。其中&lt;script type=&quot;math/tex&quot;&gt;k \ge 1, b \ge 1&lt;/script&gt;。&lt;/p&gt;

&lt;p&gt;对于一颗树有n个节点，并满足我们上述所说的约束，那么有：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;子树节点数 \le min\{ \frac{n - 1 - b}{k + 1} * k + b, n - 1 \}&lt;/script&gt;

&lt;p&gt;为了便于描述，我们把整棵数的节点数记为&lt;script type=&quot;math/tex&quot;&gt;n_0&lt;/script&gt;，并把第二层节点最大可能数值记为&lt;script type=&quot;math/tex&quot;&gt;n_1&lt;/script&gt;，然后是&lt;script type=&quot;math/tex&quot;&gt;n_2&lt;/script&gt;，&lt;script type=&quot;math/tex&quot;&gt;n_3&lt;/script&gt;…&lt;/p&gt;

&lt;p&gt;那么上述公式转为：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;n_{i+1} \le min\{\frac{n_i-1-b}{k+1} * k + b, n\_i - 1\}&lt;/script&gt;

&lt;p&gt;然后就是引入我们最喜欢的换元大法，另 &lt;script type=&quot;math/tex&quot;&gt;m_i = n_i + k - b&lt;/script&gt;，上式转换为：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;m_{i+1} = n_{i+1} + k - b \le \frac{n_i-1-b}{k+1} * k + k = m_i * \frac{k}{k+1}&lt;/script&gt;

&lt;p&gt;因此：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;m_i \le m_0{(\frac{k}{k+1})}^i&lt;/script&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;n_i \le (n_0 + k - b){(\frac{k}{k+1})}^i - k + b&lt;/script&gt;

&lt;p&gt;当&lt;script type=&quot;math/tex&quot;&gt;n_0 + k - b \le 0&lt;/script&gt;时，树的高度&lt;script type=&quot;math/tex&quot;&gt;% &lt;![CDATA[
h &lt;= n_0 &lt;= b - k %]]&gt;&lt;/script&gt; 是一个常数。&lt;/p&gt;

&lt;p&gt;当&lt;script type=&quot;math/tex&quot;&gt;n_0 + k - b &gt; 0&lt;/script&gt;时，我们希望上面式子里的指数部分&amp;lt;=1，因此有：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;% &lt;![CDATA[
(n_0 + k - b){(\frac{k}{k+1})}^i &lt;= 1 %]]&gt;&lt;/script&gt;

&lt;p&gt;这时，两侧取对数，我们可以得到：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;i \ge \frac{log_2(n_0 + k - b)}{log_2{\frac{k+1}{k}}}&lt;/script&gt;

&lt;p&gt;因此我们知道：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;h \le \frac{log_2(n_0 + k - b)}{log_2{\frac{k+1}{k}}} + max\{b - k, 0\}&lt;/script&gt;

&lt;p&gt;上式中，除了log内部的部分，其他都是常数，因此当我们引入约束&lt;script type=&quot;math/tex&quot;&gt;% &lt;![CDATA[
L &lt;= kR + b, R &lt;= kL + b %]]&gt;&lt;/script&gt;时，我们可以获得一棵平衡树。&lt;/p&gt;

&lt;p&gt;当然，有了上面两个部分还不够，我们还需要引入一个更困难的问题：&lt;/p&gt;

&lt;h3 id=&quot;当树进行了插入删除操作后我们必须在最多olog2n次调整内让树恢复约束&quot;&gt;当树进行了插入/删除操作后，我们必须在最多O(log2n)次调整内，让树恢复约束&lt;/h3&gt;

&lt;p&gt;也就是说，上面两个条件虽然能够建立一棵平衡树，但是如果说我们每次插入/删除后，我们需要花费O(n)的时间去重新让树平衡，那么我们的数据结构显然也是没有意义的。&lt;/p&gt;

&lt;p&gt;这步也是我们设计的最核心的地方，我们下面要继续讨论。&lt;/p&gt;

&lt;h2 id=&quot;2-树的旋转以及计数维护&quot;&gt;2. 树的旋转，以及计数维护&lt;/h2&gt;

&lt;p&gt;当我们讨论平衡树时，自然的我们需要去讨论树的旋转和等价。&lt;/p&gt;

&lt;p&gt;在一般的树中，我们认为只有两棵树在结构上完全一致的，我们才认为这两棵树是等价的。显然，如果我们继续沿用这个主意的话，一棵不平衡的树我们歪脑袋也不可能给调平衡了。&lt;/p&gt;

&lt;p&gt;因此，一般在平衡树中我们认为：如果两颗树的中序遍历是一致的，我们就认为两棵树是等价的。&lt;/p&gt;

&lt;p&gt;OK，基于这一点，我们有了树的旋转（盗图wikipedia，点&lt;a href=&quot;https://en.wikipedia.org/wiki/Tree_rotation&quot;&gt;链接&lt;/a&gt;还可以看动态图）：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/posts/how-to-design-a-balance-tree-zh/Tree_rotation.png&quot; alt=&quot;Rotate tree&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在上面的旋转过程中，A仍然在P的左侧，B仍然在P、Q中间，C仍然在Q的右侧，因此旋转后，树的中序遍历没有发生变化。&lt;/p&gt;

&lt;p&gt;由于我们在每一个节点上附加了子树尺寸信息，因此我们需要重新调整附加的子树信息，&lt;strong&gt;为了叙述的更加简洁，我们后面的文章里直接用节点的字母来代表节点的size。并且用’来代表旋转后的节点尺寸，例如P’，Q’。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A，B，C以及A，B，C的所有子节点都未发生变动。&lt;/p&gt;

&lt;p&gt;当树发生右转时，Q变为P的子节点 &lt;script type=&quot;math/tex&quot;&gt;Q' = B + C + 1&lt;/script&gt;&lt;/p&gt;

&lt;p&gt;变为新的根节点 &lt;script type=&quot;math/tex&quot;&gt;P' = A + Q' + 1&lt;/script&gt;&lt;/p&gt;

&lt;p&gt;由于树的旋转是对称的，因此我们在下面的讨论中，只需要讨论一种旋转，而另一种情况下是等价的。&lt;/p&gt;

&lt;h2 id=&quot;3-k和b的选择&quot;&gt;3. k和b的选择&lt;/h2&gt;

&lt;p&gt;在上面的文章中，我们讨论了当树满足约束 &lt;script type=&quot;math/tex&quot;&gt;L \le k R + b, R \le k L + b&lt;/script&gt; 时，树为平衡树。这里我们显然要选择一对合适的k和b来继续我们的设计。&lt;/p&gt;

&lt;p&gt;由于我们上面的证明假定了&lt;script type=&quot;math/tex&quot;&gt;k \ge 1&lt;/script&gt;，那么我们从&lt;script type=&quot;math/tex&quot;&gt;k = 1&lt;/script&gt;开始考虑。&lt;/p&gt;

&lt;p&gt;首先，&lt;script type=&quot;math/tex&quot;&gt;k = 1&lt;/script&gt;看起来不是那么的合适。当&lt;script type=&quot;math/tex&quot;&gt;k = 1&lt;/script&gt;时，我们如果一直向尾部追加元素，每插入n个元素，我们就会导致根节点发生&lt;script type=&quot;math/tex&quot;&gt;\frac{n}2&lt;/script&gt;次旋转，显然这个太频繁了，能不能行得通暂时不说，光效率看起来就不太合适。&lt;/p&gt;

&lt;p&gt;从技术上来说，我们当然可以取&lt;script type=&quot;math/tex&quot;&gt;k&lt;/script&gt;为小数，例如&lt;script type=&quot;math/tex&quot;&gt;k = 1.5&lt;/script&gt;。但是作为一个程序员，显然的，没有人会跟自己过不去。&lt;/p&gt;

&lt;p&gt;那么我们暂且取&lt;script type=&quot;math/tex&quot;&gt;k=2&lt;/script&gt;。&lt;/p&gt;

&lt;p&gt;然后我们需要考虑&lt;script type=&quot;math/tex&quot;&gt;b&lt;/script&gt;，前面我们已经证明了&lt;script type=&quot;math/tex&quot;&gt;b \ge 1&lt;/script&gt;，感觉&lt;script type=&quot;math/tex&quot;&gt;b = 1&lt;/script&gt;看起来还是蛮可行的。&lt;/p&gt;

&lt;p&gt;OK，我们暂时考虑取&lt;script type=&quot;math/tex&quot;&gt;k = 2, b = 1&lt;/script&gt;。&lt;/p&gt;

&lt;p&gt;那么我们有：&lt;script type=&quot;math/tex&quot;&gt;L \le 2 R + 1, R \le 2 L + 1&lt;/script&gt;&lt;/p&gt;

&lt;p&gt;注：&lt;strong&gt;后面的文章中可以看到，事实证明k=2是行不通的，囧。但是毕竟这篇文章是指导如何设计数据结构的，而不是说，这个数据结构是可以的，因此我把不可行的部分也保留下来了，以便与同学们了解整个设计过程&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;4-插入时的维护&quot;&gt;4. 插入时的维护&lt;/h2&gt;

&lt;p&gt;这时，我们首先考虑一种较为简单的情况，维护插入。&lt;/p&gt;

&lt;h3 id=&quot;第一种旋转&quot;&gt;第一种旋转&lt;/h3&gt;

&lt;p&gt;OK，我们重新摆出从wiki盗的图：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/posts/how-to-design-a-balance-tree-zh/Tree_rotation_Q.png&quot; alt=&quot;Rotate tree&quot; /&gt;&lt;/p&gt;

&lt;p&gt;假如说我们进行了某次插入后，在Q节点发生失衡（约束冲突）。为了简化下面的讨论，我们假定插入发生在Q的左侧，如果插入发生在Q的右侧，那么只需要把下面证明中所有的部分翻转过来即可。&lt;/p&gt;

&lt;p&gt;当插入发生在Q的左侧，并发生了冲突时，显然是发生了 &lt;script type=&quot;math/tex&quot;&gt;P &gt; 2 C + 1&lt;/script&gt;。由于我们每次只插入1个元素，因此可知，此时：&lt;script type=&quot;math/tex&quot;&gt;P = 2 C + 2&lt;/script&gt;&lt;/p&gt;

&lt;p&gt;OK，我们先试试直接向右旋转。&lt;/p&gt;

&lt;p&gt;旋转发生后，我们发现A、B、C以及它们的所有字节点都没有发生变换。显然A、B、C以及它们的所有子节点原来满足约束，现在肯定也满足约束。&lt;/p&gt;

&lt;p&gt;我们先来看Q节点。&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;B = P - 1 - A \le P - 1 = 2 C + 1&lt;/script&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;2 B + 1 \ge 2 \frac{P - 2}{3} + 1 = \frac43 C + 1 &gt;= C&lt;/script&gt;

&lt;p&gt;运气不错，旋转后的Q节点是平衡的。&lt;/p&gt;

&lt;p&gt;再来看看P节点。&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;2 Q' + 1 = 2(B + C + 1) + 1 \gt 2 C + 2 = P \gt A&lt;/script&gt;

&lt;p&gt;最后我们只需要再证明&lt;script type=&quot;math/tex&quot;&gt;2 A + 1 \ge Q'&lt;/script&gt;就大功告成了。。。&lt;/p&gt;

&lt;p&gt;很可惜，高兴的太早了。。。。上面这个是不一定成立的。。。当&lt;script type=&quot;math/tex&quot;&gt;A = 1, B = 3, C = 1&lt;/script&gt;时，来回的转都没有用。。。。那怎么办？？？&lt;/p&gt;

&lt;p&gt;其实观察一下上面这种情况就知道，这个问题发生的核心原因是，B节点太大了，但是旋转的时候，只能把B节点从一侧挪到另一侧，放左边时，左边太大，放右边时，右边又炸了。所以我们必须想办法给B子树拆了，否则这个问题没法解决。&lt;/p&gt;

&lt;p&gt;damn，我们引入第二种旋转。。。&lt;/p&gt;

&lt;h3 id=&quot;第二种旋转&quot;&gt;第二种旋转&lt;/h3&gt;

&lt;p&gt;首先，我们必须明确一点，如果第一种旋转已经OK了，那明显就不需要第二种旋转了。因此我们在进行第二种旋转时，有一个前提条件：&lt;script type=&quot;math/tex&quot;&gt;% &lt;![CDATA[
2 A + 1 &lt; Q' = B + C + 1 %]]&gt;&lt;/script&gt;，也就是 &lt;script type=&quot;math/tex&quot;&gt;% &lt;![CDATA[
2 A &lt; B + C %]]&gt;&lt;/script&gt;。&lt;/p&gt;

&lt;p&gt;Sadly，这次没地方可以盗图了，自己画吧。。。。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/posts/how-to-design-a-balance-tree-zh/Tree_rotation_Q2.png&quot; alt=&quot;Rotate tree&quot; /&gt;&lt;/p&gt;

&lt;p&gt;哈哈，凑合看，大家见谅。（欢迎提供好看的图，lol）&lt;/p&gt;

&lt;p&gt;显然，第二种旋转要进行的话，我们有个前提条件，就是B节点要存在。。。&lt;/p&gt;

&lt;p&gt;我们假设 &lt;script type=&quot;math/tex&quot;&gt;A \ge B&lt;/script&gt;，那么有：&lt;script type=&quot;math/tex&quot;&gt;2A - B \ge A \ge \frac{P - 1}2 = \frac{2C + 1}2 &gt; C&lt;/script&gt;。和我们进行第二种旋转的前提条件冲突。因此我们有 &lt;script type=&quot;math/tex&quot;&gt;% &lt;![CDATA[
A &lt; B %]]&gt;&lt;/script&gt;。因此&lt;script type=&quot;math/tex&quot;&gt;B &gt; 0&lt;/script&gt;，所以B节点必然存在。&lt;/p&gt;

&lt;p&gt;接下来就是漫漫长征啊，我们需要证明P, Q, B节点都是符合约束的。&lt;/p&gt;

&lt;p&gt;先看节点P：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;2A + 1 \ge B &gt; D&lt;/script&gt;

&lt;p&gt;然后。。。。。。这里我想插入一个超diao的表情啊，，，，lol&lt;/p&gt;

&lt;p&gt;大家会发现无论如何也证明不了 &lt;script type=&quot;math/tex&quot;&gt;2D + 1 \ge A&lt;/script&gt;。&lt;/p&gt;

&lt;p&gt;对的，因为这tmd的的确就是不成立的。。。。&lt;/p&gt;

&lt;p&gt;我们另C = 1000, P = 2002, A = 1000, B = 1001, D = 333, E = 667。显然这是一组“好好”的反例啊。&lt;/p&gt;

&lt;p&gt;这时候怎么办？有两种思路：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;方案一：我们接着往下修P节点，但是必须证明接着往下修在有限步数内一定能给修好。&lt;/li&gt;
  &lt;li&gt;方案二：调整约束&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在上次失败的时候，为什么我们不尝试调整约束，而是继续往下修呢？因为上次的情况调整约束是不能解决问题的。因为无论我们如何调整约束，当B过大时，通过第一种旋转永远无法解决B过大的问题。&lt;/p&gt;

&lt;p&gt;但是这次的情况和上次有所不同。核心原因是调整系数k会影响我们这次的情况。&lt;/p&gt;

&lt;p&gt;经过短暂的尝试后，个人放弃了往方案一方向的探索。（不等于方案一一定行不通）。&lt;/p&gt;

&lt;h2 id=&quot;5-调整参数k&quot;&gt;5. 调整参数k&lt;/h2&gt;

&lt;p&gt;观察到现在问题的核心是B太小，而A太大导致的。那么我们能不能尝试调整我们的参数来绕开这个问题？&lt;/p&gt;

&lt;p&gt;通过上面的证明，我们知道第二种旋转触发时&lt;script type=&quot;math/tex&quot;&gt;A \le C&lt;/script&gt;，因此&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;B = P - A - 1 = kC + 1 - A \ge kA + 1 - A = (k-1)A + 1&lt;/script&gt;

&lt;p&gt;由于我们第一次尝试时，选择的k等于2，因此导致A和B数值上过于接近，进而导致第二类旋转不能保证约束。通过上面式子我们可以观察到，如果我们把k从2改为3，可以快速的拉开A和B的差距。&lt;/p&gt;

&lt;p&gt;但是具体需要改多大，还需要进一步的尝试。做了一些简单的尝试后，我们把k从2改为3，拿到我们第二组常数：&lt;script type=&quot;math/tex&quot;&gt;k=3, b=1&lt;/script&gt;&lt;/p&gt;

&lt;h2 id=&quot;6-再来一次lol&quot;&gt;6. 再来一次（lol）&lt;/h2&gt;

&lt;p&gt;由于我们在上面的设计中，调整了约定，因此，我们现在面临着一个非常“nice”的情况。&lt;/p&gt;

&lt;p&gt;对的，上面的所有证明都要重来一次。。。。。&lt;/p&gt;

&lt;p&gt;实际上，在设计数据结构的时候，就是这样反复的过程。每一次约束的修改，就像是推翻重做一样。任意一个非常小的约束修改，都有可能导致新的问题。而数据结构设计，就是在夹缝之中寻找出一条小径。&lt;/p&gt;

&lt;p&gt;实际上在我设计这个数据结构时，我还遇到了一次重新证明。也就是在处理删除数据时，我又一次重新证明来过了一遍。&lt;/p&gt;

&lt;p&gt;本来我还想做一个冷笑话，放进去大家乐乐的，哈哈哈。考虑到我现在打字打的快呕心沥血了，也就不给自己再找罪受了。&lt;/p&gt;

&lt;h3 id=&quot;考虑删除情况&quot;&gt;考虑删除情况&lt;/h3&gt;

&lt;p&gt;先重新贴份图方便看的：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/posts/how-to-design-a-balance-tree-zh/Tree_rotation_Q.png&quot; alt=&quot;Rotate tree&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在我们前面的推演中，我们假定了我们是插入左侧导致了约束被破坏。由于我们每次插入时最多只插入一个元素，因此当&lt;script type=&quot;math/tex&quot;&gt;P &gt; 3C + 1&lt;/script&gt;，我们必然有：&lt;script type=&quot;math/tex&quot;&gt;P = 3C + 2&lt;/script&gt;。&lt;/p&gt;

&lt;p&gt;这次我们同时把删除的也考虑了，其实在我自己推演时，我是先推演了插入的维护，再推演删除的维护。&lt;/p&gt;

&lt;p&gt;但是考虑到我现在手快要抽筋了，咱们还是能省则省吧。&lt;/p&gt;

&lt;p&gt;当我们在考虑插入时，当树的左侧L插入一个元素，那么显然不可能破坏&lt;script type=&quot;math/tex&quot;&gt;3L + 1 \ge R&lt;/script&gt;这个约束。因此只可能破坏&lt;script type=&quot;math/tex&quot;&gt;3R + 1 &gt;= L&lt;/script&gt;这种情况。我们将不等式稍作调整，有&lt;script type=&quot;math/tex&quot;&gt;% &lt;![CDATA[
L - (3R + 1) &lt;= 0 %]]&gt;&lt;/script&gt;。&lt;/p&gt;

&lt;p&gt;当失衡发生时，有&lt;script type=&quot;math/tex&quot;&gt;L - (3R + 1) &gt; 0&lt;/script&gt;。这时，我们可以用&lt;script type=&quot;math/tex&quot;&gt;L - (3R + 1)&lt;/script&gt;来衡量失衡的程度。显然，失衡程度越大，想要一次性修复的难度越大。由于每次失衡后，我们会立即进行修复。因此当发生插入失衡时，&lt;script type=&quot;math/tex&quot;&gt;L - (3R + 1) \le 1&lt;/script&gt;。&lt;/p&gt;

&lt;p&gt;其实当发生删除时，情况非常的类似。同样为了便于讨论，我们只讨论删除右侧的情况，也就是减少R。同样我们每次失衡后会立即进行修复，因此当发生删除失衡时，我们有&lt;script type=&quot;math/tex&quot;&gt;L - (3R + 1) \le 3&lt;/script&gt;&lt;/p&gt;

&lt;p&gt;回到我们图片中的符号，我们有：&lt;script type=&quot;math/tex&quot;&gt;% &lt;![CDATA[
3C + 1 &lt; P \le 3C + 4 %]]&gt;&lt;/script&gt;。其实这个不等式的调整，对于大部分的证明来说都是不影响的。但是还是有部分的东西让我折腾了很久。&lt;/p&gt;

&lt;h3 id=&quot;第一种旋转-1&quot;&gt;第一种旋转&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/assets/posts/how-to-design-a-balance-tree-zh/Tree_rotation_Q.png&quot; alt=&quot;Rotate tree&quot; /&gt;&lt;/p&gt;

&lt;p&gt;由于调整了插入元素个数的不等式，使得我们这次的证明变得繁琐了很多。首先我们继续考虑Q’下的情况。&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;3B + 1 \ge 3\frac{P-2}{4} + 1 \ge \frac94 C + 1 &gt; C&lt;/script&gt;

&lt;p&gt;但是我们证明&lt;script type=&quot;math/tex&quot;&gt;3C + 1 \ge B&lt;/script&gt;时，我们遇到了个很大的麻烦：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;3C + 1 \ge P - 3 \ge \frac{B-1}3 * 4 + 2 - 3 = \frac43B - \frac73&lt;/script&gt;

&lt;p&gt;当上面的式子中，&lt;script type=&quot;math/tex&quot;&gt;B \ge 7&lt;/script&gt;时，&lt;script type=&quot;math/tex&quot;&gt;3C + 1 \ge B&lt;/script&gt;即可成立。&lt;/p&gt;

&lt;p&gt;当然，我们可以想办法继续调整参数绕开这个问题，例如放宽b，使得b=2。但是考虑到这里B仅仅在较小的时候有例外。因此我们做个简单的穷举：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;C = 0时，B &amp;lt;= 2，存在一个反例&lt;/li&gt;
  &lt;li&gt;C = 1时，B &amp;lt;= 4，全部满足约束&lt;/li&gt;
  &lt;li&gt;C = 2时，B &amp;lt;= 7，全部满足约束&lt;/li&gt;
  &lt;li&gt;C &amp;gt;= 3时，B取[0,6]满足约束，B取7以上也满足约束&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;综上可知，仅当C = 0，B = 2时，我们可以找到一个冲突。但这个问题不大，如果遇到这种情况时，我们再对Q’进行一次向右旋转即可解决问题。&lt;/p&gt;

&lt;p&gt;OK，我们来继续看P’下的情况。&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;3Q' + 1 &gt; 3B + 1 \ge A&lt;/script&gt;

&lt;p&gt;最后，我们又回到了上次的场景，我们无法保证&lt;script type=&quot;math/tex&quot;&gt;3A + 1 \ge Q'&lt;/script&gt;。为此我们分两种情况处理：当满足&lt;script type=&quot;math/tex&quot;&gt;3A + 1 \ge Q'&lt;/script&gt;时，我们使用第一种旋转进行修正，当不满足这个情况时，我们祭出第二种旋转。&lt;/p&gt;

&lt;p&gt;同样注意，当进行第二种旋转时，我们有：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;% &lt;![CDATA[
3A + 1 &lt; Q' = B + C + 1 %]]&gt;&lt;/script&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;% &lt;![CDATA[
3A &lt; B + C %]]&gt;&lt;/script&gt;

&lt;h3 id=&quot;第二种旋转-1&quot;&gt;第二种旋转&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/assets/posts/how-to-design-a-balance-tree-zh/Tree_rotation_Q2.png&quot; alt=&quot;Rotate tree&quot; /&gt;&lt;/p&gt;

&lt;p&gt;不知道有多少人能看到这里哈，不过一个好消息是，我们的证明就要结束了。&lt;/p&gt;

&lt;p&gt;为了方便后面的证明，我们先在这里把一些通用性的东西证明出来：&lt;/p&gt;

&lt;p&gt;首先我们有第二种旋转的触发条件：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;% &lt;![CDATA[
3A &lt; B + C %]]&gt;&lt;/script&gt;

&lt;p&gt;通过这个我们可以推导出：&lt;script type=&quot;math/tex&quot;&gt;A \le C&lt;/script&gt;，证明如下：&lt;/p&gt;

&lt;p&gt;假设&lt;script type=&quot;math/tex&quot;&gt;A &gt; C&lt;/script&gt;，则有 &lt;script type=&quot;math/tex&quot;&gt;A \ge C + 1&lt;/script&gt;，则有 &lt;script type=&quot;math/tex&quot;&gt;3A \ge 3C + 3 \ge P - 1 = A + B &gt; B + C&lt;/script&gt;，和上式冲突，因此有：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;A \le C&lt;/script&gt;

&lt;p&gt;由于&lt;script type=&quot;math/tex&quot;&gt;P &gt; 3C + 1&lt;/script&gt;，因此&lt;script type=&quot;math/tex&quot;&gt;A + B &gt; 3C \ge 3A&lt;/script&gt;，因此：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;B &gt; 2A \\ B \ge 2A + 1&lt;/script&gt;

&lt;p&gt;由于&lt;script type=&quot;math/tex&quot;&gt;P = A + B + 1 &gt; 3C + 1&lt;/script&gt;，因此：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;A + B &gt; 3C \\ A + B \ge 3C + 1&lt;/script&gt;

&lt;p&gt;由于&lt;script type=&quot;math/tex&quot;&gt;A \ge \frac{P - 2}4&lt;/script&gt;，因此：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;A \ge \frac{3C}4&lt;/script&gt;

&lt;p&gt;OK，现在回到证明P’节点是满足约束的。&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;3A + 1 \ge B &gt; D&lt;/script&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;3D + 1 \ge 3 * \frac{B-2}{4} + 1 \ge 3 * \frac{2A - 1}{4} + 1 = \frac32 A + \frac14 &gt; A&lt;/script&gt;

&lt;p&gt;然后是证明Q’节点是满足约束的。&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;3C + 1 \ge 3A + 1 \ge B &gt; E&lt;/script&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;% &lt;![CDATA[
\begin{align}
3E + 1 &amp;\ge 3 * \frac{B-2}4 + 1 \\ &amp;= \frac34 B - \frac12 \\
&amp;=\frac12 B + \frac14 B - \frac12 \\
&amp;\ge \frac12 B + \frac14(2A + 1) - \frac12 \\
&amp;= \frac12(A+B)-\frac14 \\
&amp;\ge \frac12(3C + 1) - \frac14 \\
&amp;= \frac32 C + \frac14 \\
&amp;&gt; C
\end{align} %]]&gt;&lt;/script&gt;

&lt;p&gt;最后证明B’节点满足约束。&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;% &lt;![CDATA[
\begin{align}
3P' + 1 &amp;= 3(A+D+1) + 1 \\
&amp;=3A + (3D + 1) + 3 \\
&amp;\ge \frac94 C + E + 3 \\
&amp;\ge C + E + 3 \\
&amp;= Q' + 2 \\
&amp;&gt; Q'
\end{align} %]]&gt;&lt;/script&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;% &lt;![CDATA[
\begin{align}
3Q' + 1 &amp;= 3(E+C+1) + 1 \\
&amp;= (3E + 1) + 3C + 3 \\
&amp;\ge D + 3A + 3 \\
&amp;\ge D + A + 3 \\ 
&amp;= P' + 2 \\
&amp;&gt; P'
\end{align} %]]&gt;&lt;/script&gt;

&lt;p&gt;最后我们要非常装Bility的写下：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;Q.E.D \tag * {$\blacksquare$}&lt;/script&gt;

&lt;h2 id=&quot;7-代码实现&quot;&gt;7. 代码实现&lt;/h2&gt;

&lt;p&gt;看完上面的乱七八糟的证明，我估计大家都已经晕了，我们新设计的数据结构到底怎么用。这里简单的汇总一下吧。&lt;/p&gt;

&lt;p&gt;首先，我们需要在节点上附加一个整型的数字，size，并维护size为节点以及所有子节点的个数。考虑到我们写这篇文章面向的读者，这里就不再累述该如何维护size了。&lt;/p&gt;

&lt;p&gt;然后，每当我们每次对树进行了修改之后，我们需要从修改处一直往上遍历，检测是否每个节点都满足约束&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left-&amp;gt;size * 3 + 1 &amp;gt;= right-&amp;gt;size &amp;amp;&amp;amp; right-&amp;gt;size * 3 + 1 &amp;gt;= left-&amp;gt;size&lt;/code&gt;。当left或者right不存在时，我们视同为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left-&amp;gt;size&lt;/code&gt;或&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right-&amp;gt;size&lt;/code&gt;等于0。&lt;/p&gt;

&lt;p&gt;如果发现某个节点不满足约束时，我们进行如下操作：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;当&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left-&amp;gt;size &amp;gt; right-&amp;gt;size&lt;/code&gt;时，我们将树从节点处，进行一次向右操作。&lt;/li&gt;
  &lt;li&gt;当&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left-&amp;gt;size &amp;lt; right-&amp;gt;size&lt;/code&gt;时，我们将树从节点处，进行一次向左操作。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;其中向左和向右为对称操作，这里简单以向右操作为例。&lt;/p&gt;

&lt;p&gt;当我们进行向右操作时，我们需要先检查是否满足&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left-&amp;gt;left-&amp;gt;size &amp;gt;= left-&amp;gt;right-&amp;gt;size + right-&amp;gt;size&lt;/code&gt;，如果满足，我们进行第一种旋转，否则我们进行第二种旋转。&lt;/p&gt;

&lt;h3 id=&quot;第一种旋转-2&quot;&gt;第一种旋转&lt;/h3&gt;

&lt;p&gt;当我们进行第一种旋转时，我们直接对节点进行向右或者向左旋转，具体旋转逻辑参见：&lt;a href=&quot;https://en.wikipedia.org/wiki/Tree_rotation&quot;&gt;Tree Rotation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;当我们在进行向右旋转时，当我们旋转完了后，我们需要检测是否&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right-&amp;gt;right-&amp;gt;size == 0 &amp;amp;&amp;amp; right-&amp;gt;left-&amp;gt;size == 2&lt;/code&gt;。如果是的话，我们需要对right再进行一次平衡，这里有可能是第一种旋转，也有可能是第二种旋转。&lt;/p&gt;

&lt;p&gt;当我们在进行向左旋转时，情况和上面镜像处理。&lt;/p&gt;

&lt;p&gt;经过操作后，可以保证节点进入满足约束状态，可以继续向上遍历。&lt;/p&gt;

&lt;p&gt;注意，当进行旋转操作时，节点附加的size数据也需要进行维护。&lt;/p&gt;

&lt;h3 id=&quot;第二种旋转-2&quot;&gt;第二种旋转&lt;/h3&gt;

&lt;p&gt;当我们进行第二种旋转时，我们需要进行两次旋转操作。这里以向右旋转为例。&lt;/p&gt;

&lt;p&gt;我们先需要对left进行一次向左旋转，然后再对节点本身做一次向右旋转。&lt;/p&gt;

&lt;p&gt;当我们在进行向左旋转时，情况和上面镜像处理。&lt;/p&gt;

&lt;p&gt;经过操作后，可以保证节点进入满足约束状态，可以继续向上遍历。&lt;/p&gt;

&lt;p&gt;同样需要注意，当进行旋转操作时，节点附加的size数据也需要进行维护。&lt;/p&gt;

&lt;h2 id=&quot;8-thanks-to&quot;&gt;8. Thanks to&lt;/h2&gt;

&lt;p&gt;最后再感谢一下”杀出一条蟹路“同学帮忙复核数学推理。:)&lt;/p&gt;</content><author><name>Chizhong Jin</name><email>pjincz@gmail.com</email></author><category term="algorithm" /><category term="data-structure" /><category term="zh" /><summary type="html">(感谢”杀出一条蟹路“同学帮忙复核数学推理) 昨天在群里和一些同学讨论算法，扯到平衡树。突然想写点东西，给同学们分享分享。但是我今天要分享的不是有多少种平衡树实现，而是如何设计一种平衡树。 希望通过这个分享，能让同学们有一些基础的认识，一种数据结构/算法是如何设计出来的。 而我们今天的话题，就是如何在树节点上，通过附加一个size来进行树平衡。 0. 开始之前 当我写这篇文章的时候，我希望对算法有所了解的同学都能看懂。数据结构设计相对于算法来说，还是有那么一点点的偏难。有些概念可能大家都是第一次接触，因此我在这里先进行一些基础概念的讲解。 约束 数据结构设计和一般性的算法有一个非常大的差异，就是： 数据结构是一个在一段时间内存在，并持续维护的结构。而不是一次性运行结束的函数调用。 这中间的最大差异是，假设我们现在定义一棵树是平衡的，那么树的高度。假设我们这时需要实现一个函数isBalanceTree(root)，那么显然除了完整的遍历整棵外，我们并没有其他什么办法。显然这个时间复杂度是O(n)的。 判断一个树是否是平衡的，这个都是O(n)的了，如果我们需要写一个函数balanceTree去平衡一棵树，这个时间复杂度至少是O(n)。 如果说我们写一棵普通的二叉树，然后每次插入/删除后，我们运行一次balanceTree我们也可以得到一棵平衡树。但是这个明显没有多大意义。还不如弄个数组，每次先定位再插入，不但简单，还来的快。 那我们如何来解决这个问题，中间的“秘密”就是约束。 所谓约束就是：数据结构在“平时”应该满足什么限制。 如果说一个数据结构，只要满足了某约束，就能带来某个我们想要的特性。而我们每次修改数据结构时，只需要很小的代价，就能保持这个约束。那么这就是我们想要的。 当然，上面的讲的太抽象了，这里我们拿个红黑树举例。 红黑树在二叉树的基础上，引入了4条约束。 节点必须是红色或者黑色 根节点是黑色 红色节点不能和红色节点相邻 从根节点出发，到任意nil节点所路过的黑节点数量一致 假如说我们记从根节点出发，黑色节点数为d，树的最大高度为h。 从约束4我们知道，d对于任意路径都一样。 从约束1和3我们知道，。 另外我们知道，对于任意红黑树， 于是，我们知道： 因此，我们从红黑树的约定中，我们推出了我们想要的平衡。 另外，红黑树对于插入/删除时，有一套完整的修正约束的方法，这里我们就不展开讲了，有兴趣的同学戳这里：维基百科-红黑树 注：上面的分析中，我们并没有用到约束2，约束2不是可选的，是红黑树上升的重要约束。 分析 接着我们需要一个合适的衡量指标去衡量我们的目标是否达成。就像是我上面举的定义，稍作放松是完全可以用的。但是这里我还是想探讨一下如何定义一个良好的衡量，用作后续的分析。 显然，“平衡树”是一棵树哈。我们这里更关心的是“平衡”。所谓的“平衡”就是：树中间的最深叶子节点的深度和最浅叶子节点的深度差的“不太大”。当然这个定义有点“不太数学”。 考虑到一个h层深度的满树能容纳的节点数n为： 因此，对于一个n个节点的树，我们能获得的最小的高度为： 我们可以把平衡树定义为： 其中k可以视为平衡系数。显然当k=1时，树不可能更“平衡”了，当k是常数时，我们访问树的任意节点的时间复杂度不超过，也就是。显然我们可以认为，这个时候树是平衡的。 由于ceil函数在分析时会给我们带来麻烦，因此我们把上面的条件弱化为： 其中k和b都是常数。这个时候访问任意节点，我们的时间复杂度仍然不超过。 需要注意的是，上面的这些定义都是仅限于这篇文章（虽然你拿出去用问题也不大）。另外在实际设计设计过程中，不需要把这些东西精确的写出来，一般模糊的给自己一个感觉：访问节点的时间最糟情况下应该是对数时间，就够了。 前面我们已经分析了红黑树是可以满足这个条件的。而且是一棵非常好的平衡树(k=2，实际上k&amp;lt;2因为我们进行了一个比较“大方”的缩放，具体数值有兴趣可以自行细算)。 如果同学们有兴趣的话，还可以对AVL树进行一下分析，AVL分析的过程中还可以出现很好玩的斐波那契数列哈哈哈。然后可以得到一个k大约等于1.44（随手算的，错了请无视）。可见AVL比红黑树平衡性要更强。 当然，平衡性更强不代表数据结构更好，虽然更强的平衡可以在访问时获得更短的路径，但是更强的平衡性同时会导致插入删除过程中更多的失衡调整。因此具体那种情况更有，还需要更多的考量。当然已经超越本文探讨的范畴了。 1. 可行性分析 我们需要分析使用子树size进行平衡的可能性。这时我们首先引入我们的第一个约束： 约束1：每个节点附带一个size = 所在节点以及所有子节点的个数 这是一个蛮简单明了的约束，显然，维护这么一个约束并不难。每次插入时，我们只需要从插入位置一直到根节点，一路更新size即可。对于删除问题也差不多。 当然，这个约束明显不足以建立平衡树，我们需要考虑在这个约束的基础上再加入更复杂的约束。这时我们需要考虑几个条件： 通过某种对左右树size的约束，对于任何情况，树是可以建立起来的。 第一个条件是明显需要满足的，否则树都不能建立起来，那还平衡个毛线。这条看起来很废话，但是实际上却不能不考虑。例如假设我们引入一个约束： 。（其中分别为左右子树大小） 这个乍一眼一看是一个蛮合理的约束，但是咱考虑一下，假设一棵树有两个节点，这棵树怎么办？ 显然我们需要1个根节点，那么我们把剩下的一个节点放入到左侧时，就不成立了。反之，就不成立了。因此，上述的约束不是一个可行的约束方案。 但是考虑一下 ，树就可以建立起来了。 通过约束，可以推导出平衡特性 这显然又是一句废话，我们的目标是设计一颗平衡树，如果树不平衡我们还玩毛线啊。因此我们需要一个证明，证明只要我们的size一直符合我们的约束，树就是平衡的。 在上面的例子中，我们用的是。但是我们在后面的研究中，系数2和1其实可能是会发生变化的，为了给后面留有余地，我们这里证明一下，如果一棵树任意节点的：，这颗树是平衡的。其中。 对于一颗树有n个节点，并满足我们上述所说的约束，那么有： 为了便于描述，我们把整棵数的节点数记为，并把第二层节点最大可能数值记为，然后是，… 那么上述公式转为： 然后就是引入我们最喜欢的换元大法，另 ，上式转换为： 因此： 当时，树的高度 是一个常数。 当时，我们希望上面式子里的指数部分&amp;lt;=1，因此有： 这时，两侧取对数，我们可以得到： 因此我们知道： 上式中，除了log内部的部分，其他都是常数，因此当我们引入约束时，我们可以获得一棵平衡树。 当然，有了上面两个部分还不够，我们还需要引入一个更困难的问题： 当树进行了插入/删除操作后，我们必须在最多O(log2n)次调整内，让树恢复约束 也就是说，上面两个条件虽然能够建立一棵平衡树，但是如果说我们每次插入/删除后，我们需要花费O(n)的时间去重新让树平衡，那么我们的数据结构显然也是没有意义的。 这步也是我们设计的最核心的地方，我们下面要继续讨论。 2. 树的旋转，以及计数维护 当我们讨论平衡树时，自然的我们需要去讨论树的旋转和等价。 在一般的树中，我们认为只有两棵树在结构上完全一致的，我们才认为这两棵树是等价的。显然，如果我们继续沿用这个主意的话，一棵不平衡的树我们歪脑袋也不可能给调平衡了。 因此，一般在平衡树中我们认为：如果两颗树的中序遍历是一致的，我们就认为两棵树是等价的。 OK，基于这一点，我们有了树的旋转（盗图wikipedia，点链接还可以看动态图）： 在上面的旋转过程中，A仍然在P的左侧，B仍然在P、Q中间，C仍然在Q的右侧，因此旋转后，树的中序遍历没有发生变化。 由于我们在每一个节点上附加了子树尺寸信息，因此我们需要重新调整附加的子树信息，为了叙述的更加简洁，我们后面的文章里直接用节点的字母来代表节点的size。并且用’来代表旋转后的节点尺寸，例如P’，Q’。 A，B，C以及A，B，C的所有子节点都未发生变动。 当树发生右转时，Q变为P的子节点 变为新的根节点 由于树的旋转是对称的，因此我们在下面的讨论中，只需要讨论一种旋转，而另一种情况下是等价的。 3. k和b的选择 在上面的文章中，我们讨论了当树满足约束 时，树为平衡树。这里我们显然要选择一对合适的k和b来继续我们的设计。 由于我们上面的证明假定了，那么我们从开始考虑。 首先，看起来不是那么的合适。当时，我们如果一直向尾部追加元素，每插入n个元素，我们就会导致根节点发生次旋转，显然这个太频繁了，能不能行得通暂时不说，光效率看起来就不太合适。 从技术上来说，我们当然可以取为小数，例如。但是作为一个程序员，显然的，没有人会跟自己过不去。 那么我们暂且取。 然后我们需要考虑，前面我们已经证明了，感觉看起来还是蛮可行的。 OK，我们暂时考虑取。 那么我们有： 注：后面的文章中可以看到，事实证明k=2是行不通的，囧。但是毕竟这篇文章是指导如何设计数据结构的，而不是说，这个数据结构是可以的，因此我把不可行的部分也保留下来了，以便与同学们了解整个设计过程 4. 插入时的维护 这时，我们首先考虑一种较为简单的情况，维护插入。 第一种旋转 OK，我们重新摆出从wiki盗的图： 假如说我们进行了某次插入后，在Q节点发生失衡（约束冲突）。为了简化下面的讨论，我们假定插入发生在Q的左侧，如果插入发生在Q的右侧，那么只需要把下面证明中所有的部分翻转过来即可。 当插入发生在Q的左侧，并发生了冲突时，显然是发生了 。由于我们每次只插入1个元素，因此可知，此时： OK，我们先试试直接向右旋转。 旋转发生后，我们发现A、B、C以及它们的所有字节点都没有发生变换。显然A、B、C以及它们的所有子节点原来满足约束，现在肯定也满足约束。 我们先来看Q节点。 运气不错，旋转后的Q节点是平衡的。 再来看看P节点。 最后我们只需要再证明就大功告成了。。。 很可惜，高兴的太早了。。。。上面这个是不一定成立的。。。当时，来回的转都没有用。。。。那怎么办？？？ 其实观察一下上面这种情况就知道，这个问题发生的核心原因是，B节点太大了，但是旋转的时候，只能把B节点从一侧挪到另一侧，放左边时，左边太大，放右边时，右边又炸了。所以我们必须想办法给B子树拆了，否则这个问题没法解决。 damn，我们引入第二种旋转。。。 第二种旋转 首先，我们必须明确一点，如果第一种旋转已经OK了，那明显就不需要第二种旋转了。因此我们在进行第二种旋转时，有一个前提条件：，也就是 。 Sadly，这次没地方可以盗图了，自己画吧。。。。 哈哈，凑合看，大家见谅。（欢迎提供好看的图，lol） 显然，第二种旋转要进行的话，我们有个前提条件，就是B节点要存在。。。 我们假设 ，那么有：。和我们进行第二种旋转的前提条件冲突。因此我们有 。因此，所以B节点必然存在。 接下来就是漫漫长征啊，我们需要证明P, Q, B节点都是符合约束的。 先看节点P： 然后。。。。。。这里我想插入一个超diao的表情啊，，，，lol 大家会发现无论如何也证明不了 。 对的，因为这tmd的的确就是不成立的。。。。 我们另C = 1000, P = 2002, A = 1000, B = 1001, D = 333, E = 667。显然这是一组“好好”的反例啊。 这时候怎么办？有两种思路： 方案一：我们接着往下修P节点，但是必须证明接着往下修在有限步数内一定能给修好。 方案二：调整约束 在上次失败的时候，为什么我们不尝试调整约束，而是继续往下修呢？因为上次的情况调整约束是不能解决问题的。因为无论我们如何调整约束，当B过大时，通过第一种旋转永远无法解决B过大的问题。 但是这次的情况和上次有所不同。核心原因是调整系数k会影响我们这次的情况。 经过短暂的尝试后，个人放弃了往方案一方向的探索。（不等于方案一一定行不通）。 5. 调整参数k 观察到现在问题的核心是B太小，而A太大导致的。那么我们能不能尝试调整我们的参数来绕开这个问题？ 通过上面的证明，我们知道第二种旋转触发时，因此 由于我们第一次尝试时，选择的k等于2，因此导致A和B数值上过于接近，进而导致第二类旋转不能保证约束。通过上面式子我们可以观察到，如果我们把k从2改为3，可以快速的拉开A和B的差距。 但是具体需要改多大，还需要进一步的尝试。做了一些简单的尝试后，我们把k从2改为3，拿到我们第二组常数： 6. 再来一次（lol） 由于我们在上面的设计中，调整了约定，因此，我们现在面临着一个非常“nice”的情况。 对的，上面的所有证明都要重来一次。。。。。 实际上，在设计数据结构的时候，就是这样反复的过程。每一次约束的修改，就像是推翻重做一样。任意一个非常小的约束修改，都有可能导致新的问题。而数据结构设计，就是在夹缝之中寻找出一条小径。 实际上在我设计这个数据结构时，我还遇到了一次重新证明。也就是在处理删除数据时，我又一次重新证明来过了一遍。 本来我还想做一个冷笑话，放进去大家乐乐的，哈哈哈。考虑到我现在打字打的快呕心沥血了，也就不给自己再找罪受了。 考虑删除情况 先重新贴份图方便看的： 在我们前面的推演中，我们假定了我们是插入左侧导致了约束被破坏。由于我们每次插入时最多只插入一个元素，因此当，我们必然有：。 这次我们同时把删除的也考虑了，其实在我自己推演时，我是先推演了插入的维护，再推演删除的维护。 但是考虑到我现在手快要抽筋了，咱们还是能省则省吧。 当我们在考虑插入时，当树的左侧L插入一个元素，那么显然不可能破坏这个约束。因此只可能破坏这种情况。我们将不等式稍作调整，有。 当失衡发生时，有。这时，我们可以用来衡量失衡的程度。显然，失衡程度越大，想要一次性修复的难度越大。由于每次失衡后，我们会立即进行修复。因此当发生插入失衡时，。 其实当发生删除时，情况非常的类似。同样为了便于讨论，我们只讨论删除右侧的情况，也就是减少R。同样我们每次失衡后会立即进行修复，因此当发生删除失衡时，我们有 回到我们图片中的符号，我们有：。其实这个不等式的调整，对于大部分的证明来说都是不影响的。但是还是有部分的东西让我折腾了很久。 第一种旋转 由于调整了插入元素个数的不等式，使得我们这次的证明变得繁琐了很多。首先我们继续考虑Q’下的情况。 但是我们证明时，我们遇到了个很大的麻烦： 当上面的式子中，时，即可成立。 当然，我们可以想办法继续调整参数绕开这个问题，例如放宽b，使得b=2。但是考虑到这里B仅仅在较小的时候有例外。因此我们做个简单的穷举： C = 0时，B &amp;lt;= 2，存在一个反例 C = 1时，B &amp;lt;= 4，全部满足约束 C = 2时，B &amp;lt;= 7，全部满足约束 C &amp;gt;= 3时，B取[0,6]满足约束，B取7以上也满足约束 综上可知，仅当C = 0，B = 2时，我们可以找到一个冲突。但这个问题不大，如果遇到这种情况时，我们再对Q’进行一次向右旋转即可解决问题。 OK，我们来继续看P’下的情况。 最后，我们又回到了上次的场景，我们无法保证。为此我们分两种情况处理：当满足时，我们使用第一种旋转进行修正，当不满足这个情况时，我们祭出第二种旋转。 同样注意，当进行第二种旋转时，我们有： 第二种旋转 不知道有多少人能看到这里哈，不过一个好消息是，我们的证明就要结束了。 为了方便后面的证明，我们先在这里把一些通用性的东西证明出来： 首先我们有第二种旋转的触发条件： 通过这个我们可以推导出：，证明如下： 假设，则有 ，则有 ，和上式冲突，因此有： 由于，因此，因此： 由于，因此： 由于，因此： OK，现在回到证明P’节点是满足约束的。 然后是证明Q’节点是满足约束的。 最后证明B’节点满足约束。 最后我们要非常装Bility的写下： 7. 代码实现 看完上面的乱七八糟的证明，我估计大家都已经晕了，我们新设计的数据结构到底怎么用。这里简单的汇总一下吧。 首先，我们需要在节点上附加一个整型的数字，size，并维护size为节点以及所有子节点的个数。考虑到我们写这篇文章面向的读者，这里就不再累述该如何维护size了。 然后，每当我们每次对树进行了修改之后，我们需要从修改处一直往上遍历，检测是否每个节点都满足约束left-&amp;gt;size * 3 + 1 &amp;gt;= right-&amp;gt;size &amp;amp;&amp;amp; right-&amp;gt;size * 3 + 1 &amp;gt;= left-&amp;gt;size。当left或者right不存在时，我们视同为left-&amp;gt;size或right-&amp;gt;size等于0。 如果发现某个节点不满足约束时，我们进行如下操作： 当left-&amp;gt;size &amp;gt; right-&amp;gt;size时，我们将树从节点处，进行一次向右操作。 当left-&amp;gt;size &amp;lt; right-&amp;gt;size时，我们将树从节点处，进行一次向左操作。 其中向左和向右为对称操作，这里简单以向右操作为例。 当我们进行向右操作时，我们需要先检查是否满足left-&amp;gt;left-&amp;gt;size &amp;gt;= left-&amp;gt;right-&amp;gt;size + right-&amp;gt;size，如果满足，我们进行第一种旋转，否则我们进行第二种旋转。 第一种旋转 当我们进行第一种旋转时，我们直接对节点进行向右或者向左旋转，具体旋转逻辑参见：Tree Rotation 当我们在进行向右旋转时，当我们旋转完了后，我们需要检测是否right-&amp;gt;right-&amp;gt;size == 0 &amp;amp;&amp;amp; right-&amp;gt;left-&amp;gt;size == 2。如果是的话，我们需要对right再进行一次平衡，这里有可能是第一种旋转，也有可能是第二种旋转。 当我们在进行向左旋转时，情况和上面镜像处理。 经过操作后，可以保证节点进入满足约束状态，可以继续向上遍历。 注意，当进行旋转操作时，节点附加的size数据也需要进行维护。 第二种旋转 当我们进行第二种旋转时，我们需要进行两次旋转操作。这里以向右旋转为例。 我们先需要对left进行一次向左旋转，然后再对节点本身做一次向右旋转。 当我们在进行向左旋转时，情况和上面镜像处理。 经过操作后，可以保证节点进入满足约束状态，可以继续向上遍历。 同样需要注意，当进行旋转操作时，节点附加的size数据也需要进行维护。 8. Thanks to 最后再感谢一下”杀出一条蟹路“同学帮忙复核数学推理。:)</summary></entry><entry><title type="html">牛刀杀鸡：用pytorch实现牛顿迭代</title><link href="https://mechanical-consciousness.com/2019/01/04/newtons-method-via-pytorch.html" rel="alternate" type="text/html" title="牛刀杀鸡：用pytorch实现牛顿迭代" /><published>2019-01-04T00:00:00+08:00</published><updated>2019-01-04T00:00:00+08:00</updated><id>https://mechanical-consciousness.com/2019/01/04/newtons-method-via-pytorch</id><content type="html" xml:base="https://mechanical-consciousness.com/2019/01/04/newtons-method-via-pytorch.html">&lt;p&gt;搞这个博客本来是打算写点机器学习文章啥的。但是没办法撒，市面上机器学习的文章实在太多了。
要写点有价值的东西又实在得下一番时间啊，然后没事干就开始水，然后这博客就彻底被我变成水桶了。&lt;/p&gt;

&lt;p&gt;既然都已经这样了，那就继续水吧，啊哈哈哈哈。&lt;/p&gt;

&lt;p&gt;这年头，谁隔三岔五不需要解个方程什么的呢。解方程，除非需要精确值，像我这种懒人，肯定首选二分法啊。方便简洁可靠，是吧，哈哈哈。但你说人吧，总有无聊的时候是吧，例如我现在。。。&lt;/p&gt;

&lt;p&gt;撸牛顿迭代大部分时候其实就是为了求值，当然也有时候是为了观察牛顿迭代的收敛。每次撸牛顿迭代唯一烦的地方就是求导。这个时候祭出pytorch的auto grad简直是爽歪歪啊。&lt;/p&gt;

&lt;p&gt;牛顿迭代公式：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)}&lt;/script&gt;

&lt;p&gt;代码：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;torch&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;newtons_method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;torch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tensor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;init&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requires_grad&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 把初始值转为一个tensor，并指定追踪梯度
&lt;/span&gt;    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;y&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;backward&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;                                    &lt;span class=&quot;c1&quot;&gt;# 反向传播
&lt;/span&gt;        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;grad&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.0000001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 终止条件，看要多少精度了
&lt;/span&gt;            &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sub_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;y&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;grad&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;                         &lt;span class=&quot;c1&quot;&gt;# 在python中x.grad保存的是dy/dx，也就是f'(x)
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;grad&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zero_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;                                  &lt;span class=&quot;c1&quot;&gt;# 清空梯度，避免干扰后续计算
&lt;/span&gt;    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;newtons_method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;             &lt;span class=&quot;c1&quot;&gt;# 算个跟号2玩玩。。。
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;1.4142135381698608
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;又水一篇，”完美”！！！&lt;/p&gt;</content><author><name>Chizhong Jin</name><email>pjincz@gmail.com</email></author><category term="zh" /><category term="水文" /><summary type="html">搞这个博客本来是打算写点机器学习文章啥的。但是没办法撒，市面上机器学习的文章实在太多了。 要写点有价值的东西又实在得下一番时间啊，然后没事干就开始水，然后这博客就彻底被我变成水桶了。 既然都已经这样了，那就继续水吧，啊哈哈哈哈。 这年头，谁隔三岔五不需要解个方程什么的呢。解方程，除非需要精确值，像我这种懒人，肯定首选二分法啊。方便简洁可靠，是吧，哈哈哈。但你说人吧，总有无聊的时候是吧，例如我现在。。。 撸牛顿迭代大部分时候其实就是为了求值，当然也有时候是为了观察牛顿迭代的收敛。每次撸牛顿迭代唯一烦的地方就是求导。这个时候祭出pytorch的auto grad简直是爽歪歪啊。 牛顿迭代公式： 代码： import torch def newtons_method(fn, init): x = torch.tensor(init * 1.0, requires_grad=True) # 把初始值转为一个tensor，并指定追踪梯度 for i in range(100): y = fn(x) y.backward() # 反向传播 if (y / x.grad / x).abs().item() &amp;lt; 0.0000001: # 终止条件，看要多少精度了 break x.data.sub_(y / x.grad) # 在python中x.grad保存的是dy/dx，也就是f'(x) x.grad.zero_() # 清空梯度，避免干扰后续计算 return x.item() print(newtons_method(lambda x: x*x - 2, 1)) # 算个跟号2玩玩。。。 1.4142135381698608 又水一篇，”完美”！！！</summary></entry><entry><title type="html">lowerBound和upperBound</title><link href="https://mechanical-consciousness.com/2018/11/10/lowerBound-and-upperBound.html" rel="alternate" type="text/html" title="lowerBound和upperBound" /><published>2018-11-10T00:00:00+08:00</published><updated>2018-11-10T00:00:00+08:00</updated><id>https://mechanical-consciousness.com/2018/11/10/lowerBound-and-upperBound</id><content type="html" xml:base="https://mechanical-consciousness.com/2018/11/10/lowerBound-and-upperBound.html">&lt;p&gt;话说lowerBound和upperBound真是一对神奇的函数。如果说一个人把quick sort写了十遍，当他第十一遍开始写quick sort的时候，那绝对叫一个轻车熟路啊，绝对是soso的。&lt;/p&gt;

&lt;p&gt;但是lowerBound和upperBound这对函数，我去年刷leetcode的时候写了n遍，简直到了闭眼都能墨写的程度。这两天又刷leetcode，拿起来竟然又一脸楞比。&lt;/p&gt;

&lt;p&gt;于是乎我打算“一劳永逸”的解决这个问题 —- 记下来，哈哈哈&lt;/p&gt;

&lt;h3 id=&quot;lowerboundupperbound-的含义&quot;&gt;lowerBound/upperBound 的含义&lt;/h3&gt;

&lt;p&gt;lowerBound/upperBound的含义是：在一个排序好的数组里找一个位置插入，使得插入后数组仍然是排序好的。&lt;/p&gt;

&lt;p&gt;lowerBound/upperBound的区别在于，lowerBound找的是最小的下标，upperBound找的是最大的下标。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;111, 222, 333
   ^    ^
   |    可以把222插入到这里，这里是upperBound
   也可以把222插入到这里，这里是lowerBound
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;实现&quot;&gt;实现&lt;/h3&gt;

&lt;p&gt;lowerBound/upperBound的实现异常的简洁。因为完美实现非常的精巧，但是里面的边界控制实在是非常的绕人，所以写一次忘一次。&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lowerBound&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;l&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// 初始化左右边界，注意这里r指向最后一个元素右侧的位置&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;l&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;                    &lt;span class=&quot;c1&quot;&gt;// 这里是判断不相等，而不是距离&amp;gt;1&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;m&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;floor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;l&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 使用floor正好避开了越界&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;                    &lt;span class=&quot;c1&quot;&gt;// 改为&amp;gt;=即是upperBound&lt;/span&gt;
            &lt;span class=&quot;nx&quot;&gt;l&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;m&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;                   &lt;span class=&quot;c1&quot;&gt;// 这里是m+1而不是m&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
            &lt;span class=&quot;nx&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;总共才11行，但不得不说，每一行都有雷。这叫一个心累啊。&lt;/p&gt;

&lt;p&gt;这个版本基本上可以成为范式版本了，因为改动任何一个细节都会多出n行废代码。&lt;/p&gt;

&lt;p&gt;无论是stl的实现，还是xxx的实现，全部是这个版本。只不过表述细节不一样。&lt;/p&gt;

&lt;p&gt;把代码中唯一的那个if语句中的 &amp;gt; 改为 &amp;gt;= 就可以获得upperBound函数。&lt;/p&gt;

&lt;h3 id=&quot;和binarysearch的关系&quot;&gt;和binarySearch的关系&lt;/h3&gt;

&lt;p&gt;binarySearch可以简单的用lowerBound或upperBound实现。但是C++里提供了独立的函数。用binarySearch函数的好处是，如果有很多的重复元素，碰到一个就可以提前中止。但是说实话这个价值真的不大，优化的价值非常的有限，反而binarySearch的实现代码里每一次有两个if语句，谁跑的快还真不好说。&lt;/p&gt;

&lt;p&gt;我个人不太喜欢binarySearch函数，而总是使用lowerBound/upperBound。&lt;/p&gt;</content><author><name>Chizhong Jin</name><email>pjincz@gmail.com</email></author><summary type="html">话说lowerBound和upperBound真是一对神奇的函数。如果说一个人把quick sort写了十遍，当他第十一遍开始写quick sort的时候，那绝对叫一个轻车熟路啊，绝对是soso的。 但是lowerBound和upperBound这对函数，我去年刷leetcode的时候写了n遍，简直到了闭眼都能墨写的程度。这两天又刷leetcode，拿起来竟然又一脸楞比。 于是乎我打算“一劳永逸”的解决这个问题 —- 记下来，哈哈哈 lowerBound/upperBound 的含义 lowerBound/upperBound的含义是：在一个排序好的数组里找一个位置插入，使得插入后数组仍然是排序好的。 lowerBound/upperBound的区别在于，lowerBound找的是最小的下标，upperBound找的是最大的下标。 111, 222, 333 ^ ^ | 可以把222插入到这里，这里是upperBound 也可以把222插入到这里，这里是lowerBound 实现 lowerBound/upperBound的实现异常的简洁。因为完美实现非常的精巧，但是里面的边界控制实在是非常的绕人，所以写一次忘一次。 function lowerBound(A, t) { var l = 0, r = A.length; // 初始化左右边界，注意这里r指向最后一个元素右侧的位置 while (l !== r) { // 这里是判断不相等，而不是距离&amp;gt;1 var m = Math.floor((l + r) / 2); // 使用floor正好避开了越界 if (t &amp;gt; A[m]) // 改为&amp;gt;=即是upperBound l = m + 1; // 这里是m+1而不是m else r = m; } return l; } 总共才11行，但不得不说，每一行都有雷。这叫一个心累啊。 这个版本基本上可以成为范式版本了，因为改动任何一个细节都会多出n行废代码。 无论是stl的实现，还是xxx的实现，全部是这个版本。只不过表述细节不一样。 把代码中唯一的那个if语句中的 &amp;gt; 改为 &amp;gt;= 就可以获得upperBound函数。 和binarySearch的关系 binarySearch可以简单的用lowerBound或upperBound实现。但是C++里提供了独立的函数。用binarySearch函数的好处是，如果有很多的重复元素，碰到一个就可以提前中止。但是说实话这个价值真的不大，优化的价值非常的有限，反而binarySearch的实现代码里每一次有两个if语句，谁跑的快还真不好说。 我个人不太喜欢binarySearch函数，而总是使用lowerBound/upperBound。</summary></entry><entry><title type="html">WSL快速设置</title><link href="https://mechanical-consciousness.com/2018/10/19/wsl-quick-setup-zh.html" rel="alternate" type="text/html" title="WSL快速设置" /><published>2018-10-19T00:00:00+08:00</published><updated>2018-10-19T00:00:00+08:00</updated><id>https://mechanical-consciousness.com/2018/10/19/wsl-quick-setup-zh</id><content type="html" xml:base="https://mechanical-consciousness.com/2018/10/19/wsl-quick-setup-zh.html">&lt;p&gt;这段时间一直在搞个unity webplayer的项目。尝试了各种组合方案，包括Windows+Linux虚拟机，Linux+unity测试版，Windows+Linux双机器。但无论怎么倒腾，都感觉难受。&lt;/p&gt;

&lt;p&gt;正好前几天过十一，就顺便倒腾了一下传说中的WSL（Windows Subsystem for Linux），没想要甚是惊艳，就顺便写下来，分享分享，当个笔记。&lt;/p&gt;

&lt;p&gt;如果实在无法避免双系统开发的话，那么WSL可能是目前最舒服的方案了。&lt;/p&gt;

&lt;h3 id=&quot;wsl优点缺点&quot;&gt;WSL优点/缺点&lt;/h3&gt;

&lt;p&gt;要说WSL最大的优点的话，那么就是无缝了。Linux程序直接在Windows上，可以直接访问Windows的磁盘，和Windows共享端口池。要说用户体验，的确是秒杀虚拟机几条街。&lt;/p&gt;

&lt;p&gt;要说缺点，如果作为一个开发系统，那么真的可以称得上完美了。如果非要找缺点，那么最大的缺点就是WSL继承了Windows的所有缺点：慢。但是我们毕竟不是只是拿来做开发么？&lt;/p&gt;

&lt;h2 id=&quot;安装设置&quot;&gt;安装/设置&lt;/h2&gt;

&lt;p&gt;WSL的安装不多说，微软自己的文档说的很详细，&lt;a href=&quot;https://docs.microsoft.com/en-us/windows/wsl/install-win10&quot;&gt;传送门在此&lt;/a&gt;。不过鉴于Windows的安装说明实在太过与罗嗦。其实就只有两步：&lt;/p&gt;

&lt;p&gt;1：管理员方式打开&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PowerShell&lt;/code&gt;，然后运行&lt;/p&gt;

&lt;div class=&quot;language-powershell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;Enable-WindowsOptionalFeature&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-Online&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-FeatureName&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;Microsoft-Windows-Subsystem-Linux&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;命令跑完了会提示要重启，跟着重启搞定了。&lt;/p&gt;

&lt;p&gt;2：点击&lt;a href=&quot;https://aka.ms/wsl-ubuntu-1804&quot;&gt;这里&lt;/a&gt;下载Ubuntu包，然后双击安装，根据提示设置账户，然后，一切搞定。&lt;/p&gt;

&lt;p&gt;这才是极致的用户体验啊，给微软的同学们点10086个赞。。。。&lt;/p&gt;

&lt;h3 id=&quot;修正默认文件夹创建权限问题&quot;&gt;修正默认文件夹创建权限问题&lt;/h3&gt;

&lt;p&gt;安装完了后，正常就会进入bash。但是这个时候，WSL默认的文件夹权限和原生的Linux不太一样（Linux默认创建文件夹为755权限，但是WSL默认创建文件夹为777）。这个会导致一系列的应用出问题，在进行下一步设置之前，必须要先处理这个问题。&lt;/p&gt;

&lt;p&gt;解决这个问题不难，在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.bashrc&lt;/code&gt;中加入：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;umask &lt;/span&gt;022
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;修改完&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.bashrc&lt;/code&gt;后，使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;. ~/.bashrc&lt;/code&gt;启用设置，或者直接关掉再重新打开ubuntu&lt;/p&gt;

&lt;h3 id=&quot;清理path变量避免干扰linux程序运行&quot;&gt;清理PATH变量，避免干扰Linux程序运行&lt;/h3&gt;

&lt;p&gt;WSL默认情况下，会自动把Windows的环境变量也转换到Linux的PATH变量中。这是一个很酷的特征，例如你在bash中运行&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$ notepad.exe&lt;/code&gt;，然后记事本就会在Windows中被启用。但是如果你在Windows中装了cygwin、mingw或者nodejs之类的，就会产生各种互相干扰，导致脚本挂的莫名奇妙的。因此清理掉这些变量也是必要的。&lt;/p&gt;

&lt;p&gt;这个问题也不难解决，在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.bashrc&lt;/code&gt;中加入：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;PATH&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$PATH&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'s/:\/mnt\/c\/[^:]*//g'&lt;/span&gt;&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;修改完&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.bashrc&lt;/code&gt;后，使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;. ~/.bashrc&lt;/code&gt;启用设置，或者直接关掉再重新打开ubuntu&lt;/p&gt;

&lt;h3 id=&quot;启用x11&quot;&gt;启用X11&lt;/h3&gt;

&lt;p&gt;然后就是跑个图形界面了，处理这个的时候，让我真的感受到了WSL的强大。因为实在是很简单。&lt;/p&gt;

&lt;p&gt;本来刚开始是想在WSL上跑KDE的，搞了好久始终有问题，最后还是暂时不自虐了，弄个xfce4吧：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;xfce4 xfce4-terminal
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后安装Windows下的X11 Server，有几个不同的选择，试来试去还是&lt;a href=&quot;https://sourceforge.net/projects/vcxsrv/&quot;&gt;VcXsrv&lt;/a&gt;最好用，一是开源免费，二是多顶层窗口的情况下可以正确的显示程序的图标。点击&lt;a href=&quot;https://sourceforge.net/projects/vcxsrv/&quot;&gt;这里&lt;/a&gt;下载安装。&lt;/p&gt;

&lt;p&gt;安装后启动VcXsrv时有一些选项，一路下一步就可以了。有个页面是问你四种启动方式：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/posts/wsl-quick-setup-zh/1.png&quot; alt=&quot;1&quot; /&gt;&lt;/p&gt;

&lt;p&gt;如果想启动整个Linux桌面的话，可以选右下角那个，这样的话所有的Linux程序就像是运行在一个虚拟机里一样，可以把整个Linux桌面都启动起来。但是个人更喜欢默认的选项。这样的话，Linux的程序看起来更像是原生的Windows程序。&lt;/p&gt;

&lt;p&gt;然后再在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.bashrc&lt;/code&gt;中加入两个环境变量&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;DISPLAY&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;:0
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;LIBGL_ALWAYS_INDIRECT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;修改完&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.bashrc&lt;/code&gt;后，使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;. ~/.bashrc&lt;/code&gt;启用设置。&lt;/p&gt;

&lt;p&gt;然后运行&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xfce4-terminal&lt;/code&gt;就可以启动xfce4的控制台了。如果想启动整个桌面玩的话，需要把上面截图中的选项设置为One Window模式，然后运行&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;startxfce4&lt;/code&gt;。&lt;/p&gt;

&lt;h3 id=&quot;修正中文显示问题&quot;&gt;修正中文显示问题&lt;/h3&gt;

&lt;p&gt;然后接下来会发现中午没法显示。这个简单，随便装个中文字体就可以。&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;fonts-wqy-&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;修正中文输入法问题&quot;&gt;修正中文输入法问题&lt;/h3&gt;

&lt;p&gt;接下里一个比较麻烦的问题是输入法。WSL下的程序是无法用Windows的输入法的。但是好消息是，可以装个Linux输入法。&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt-get &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;fcitx fcitx-googlepinyin
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;安装完后，把需要的环境变量先丢入&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.bashrc&lt;/code&gt;&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;XMODIFIERS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;@im&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;fcitx
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;GTK_IM_MODULE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;fcitx
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;QT_IM_MODULE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;fcitx
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后是再一次的应用配置，囧，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;. ~/.bashrc&lt;/code&gt;。接着启动输入法。&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;dbus-launch&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;     &lt;span class=&quot;c&quot;&gt;# 启动fcitx前必须先启动一个用户级的dbus&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;fcitx &amp;amp;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个时候输入法应该还是不能用，因为没有加入中文语言。打开输入法配置&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;fcitx-config-gtk3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;并把googlepinyin设置进来。&lt;/p&gt;

&lt;p&gt;如果你Win10系统有英文语言的话，按Win+Space切换到英语语言，然后按Ctrl+Space应该就能切出fcitx了。如果Win10只有中文的话，Ctrl+Space应该是会互相冲突的（我没试过），去系统里添加语言就可以了。&lt;/p&gt;

&lt;p&gt;最后贴个图：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/posts/wsl-quick-setup-zh/5.png&quot; alt=&quot;5&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;隐藏不想要的原始命令行窗口&quot;&gt;隐藏不想要的原始命令行窗口&lt;/h3&gt;

&lt;p&gt;写完整个文章后，总觉得好像缺了点啥，哈哈哈。原来是我太习惯RBTray了。都不觉得是个事了。&lt;/p&gt;

&lt;p&gt;WSL会默认启动一个bash的窗口，这个窗口”明显的不好用“啊。否则折腾这么久，装个xfce4-terminal干啥呢？对吧。&lt;/p&gt;

&lt;p&gt;干掉这个窗口有两个办法，方案1，换用setsid启动&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xfce4-terminal&lt;/code&gt;：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;setsid xfce4-terminal
^D                     &lt;span class=&quot;c&quot;&gt;# 按下Ctrl-d，退掉这个不想要的bash，这个时候xfce4-terminal就不会跟着退掉了&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;另一个方案，就是装一个&lt;a href=&quot;http://rbtray.sourceforge.net/&quot;&gt;RBTray&lt;/a&gt;，这伙真是个神器啊，只要几十k，所有不想要的窗口统统隐藏掉，最典型的就是vmware，免费版不允许headless，无所谓，直接隐藏掉。哈哈哈。&lt;/p&gt;

&lt;h2 id=&quot;自动x11输入法系统服务&quot;&gt;自动X11，输入法，系统服务&lt;/h2&gt;

&lt;p&gt;搞到这里，基本上的东西都可以使用了，只不过还是稍微有点繁琐，每次都需要先启动X11，再启动输入法，再启动一系列的服务。有的同学可能有洁癖，下面的可以直接忽略。&lt;/p&gt;

&lt;h3 id=&quot;自动启动x11&quot;&gt;自动启动X11&lt;/h3&gt;

&lt;p&gt;启动XLauncher，选择喜好的配置后，一路走到最后一步。点击保存配置，保存到任意一个位置即可。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/posts/wsl-quick-setup-zh/2.png&quot; alt=&quot;2&quot; /&gt;&lt;/p&gt;

&lt;p&gt;复制一下XLauncher的快捷方式，右键属性，打开设置，在目标后面追加：“ -run xluancher配置保存路径\config.xlaunch”。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/posts/wsl-quick-setup-zh/3.png&quot; alt=&quot;3&quot; /&gt;&lt;/p&gt;

&lt;p&gt;退出现在的XLauncher，双击一下快捷方式，验证一下是不是和预期一致。&lt;/p&gt;

&lt;p&gt;将快捷方式移入：C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/posts/wsl-quick-setup-zh/4.png&quot; alt=&quot;4&quot; /&gt;&lt;/p&gt;

&lt;p&gt;打开任务管理器的startup面板可以再确认一下，That’s All。&lt;/p&gt;

&lt;h3 id=&quot;自动启动输入法&quot;&gt;自动启动输入法&lt;/h3&gt;

&lt;p&gt;自启动输入法稍微有点蛋疼，因为WSL不遵循标准的系统启动流程。网上有很多关于这方面的讨论，很多人建议用vb script解决这个问题。vbs这明显不是我的菜啊。而且即使是vbs方案，本身也算不上多漂亮。&lt;/p&gt;

&lt;p&gt;本着实用主义至上原则，咱还是直接瞎搞吧：&lt;/p&gt;

&lt;p&gt;打开&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.bashrc&lt;/code&gt;并加入：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; pgrep fcitx &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /dev/null&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt;   &lt;span class=&quot;c&quot;&gt;# 如果没有找到fcitx的话，就启动个新的&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;dbus-launch&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
    fcitx &amp;amp;
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;自启动服务&quot;&gt;自启动服务&lt;/h3&gt;

&lt;p&gt;然后作为开发，就是要启动各种稀奇古怪的东西了，例如mysql啥的。我个人觉得这个手工启动一下还是能接受的。毕竟也不是每次打开wsl都是想干后端的，对吧。但是如果这个都懒得动手的话，继续改造咱的bashrc即可。（反正都这么脏了，哈哈哈）&lt;/p&gt;

&lt;p&gt;和输入法问题相比，有点区别的地方在于，启动服务需要su权限，这个也好说，直接google一下sudo nopassword自然一大把的解。&lt;/p&gt;

&lt;h3 id=&quot;没了&quot;&gt;没了&lt;/h3&gt;

&lt;p&gt;搞完这几个步骤之后，基本上WSL就能像一个正常的Linux一样使用了。剩下的么，喜欢vim的装vim，喜欢emacs的装emacs。纯bash党装上tmuxinator。&lt;/p&gt;

&lt;h2 id=&quot;wsl特性&quot;&gt;WSL特性&lt;/h2&gt;

&lt;h3 id=&quot;ip地址共享&quot;&gt;IP地址共享&lt;/h3&gt;

&lt;p&gt;这个应该是WSL最让我吃惊的地方了，在WSL里的程序可以和Windows共享IP。因此如果在WSL下安装并启动了nginx的话，完全可以在Windows的浏览器里访问&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;127.0.0.1&lt;/code&gt;。相当的爽。&lt;/p&gt;

&lt;h3 id=&quot;磁盘访问&quot;&gt;磁盘访问&lt;/h3&gt;

&lt;p&gt;在Linux下访问Windows的磁盘非常的容易，全部挂载在/mnt下，例如/mnt/c就是c盘，/mnt/d就是d盘。&lt;/p&gt;

&lt;p&gt;但是在Linux下访问Windows的磁盘有个问题，就是文件权限不太对，复制到Linux下全有可执行权限，这倒是一个相当烦的问题。&lt;/p&gt;

&lt;p&gt;在Windows下访问Linux的磁盘也非常的容易，全部在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C:\Users\&amp;lt;UserName&amp;gt;\AppData\Local\Packages\CanonicalGroupLimited.Ubuntu18.04onWindows_79rhkp1fndgsc\LocalState\rootfs&lt;/code&gt;下（取决于装的系统，这里是Ubuntu）。但是WSL官方强烈建议不要直接访问这个目录，因此还是别自找麻烦了。&lt;/p&gt;

&lt;h3 id=&quot;磁盘权限&quot;&gt;磁盘权限&lt;/h3&gt;

&lt;p&gt;在WSL中，除了/mnt挂载的Windows磁盘，全部拥有Linux标准的权限，因此和Linux的兼容性很好。但是selinux之类的也是不要想太多了。&lt;/p&gt;

&lt;p&gt;/mnt目录下挂载的Windows磁盘全部是Windows的权限。例如创建的文件的权限全是777。这个地方需要特别的小心，例如：不要混用Windows和Linux的git仓库，因为git在入库时会追溯文件的可执行权限。&lt;/p&gt;

&lt;h2 id=&quot;colinux&quot;&gt;colinux&lt;/h2&gt;

&lt;p&gt;用起WSL感觉真是棒的不行，但又不得不提起一个曾经叫&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;colinux&lt;/code&gt;的项目。但只可惜&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;colinux&lt;/code&gt;最终在32/64位系统交替中挂掉了。这些年一直是缅怀的不行啊。&lt;/p&gt;

&lt;p&gt;没想到过了几年微软竟然自己投人，搞了这么个神器。真是峰回路转，哈哈哈。&lt;/p&gt;</content><author><name>Chizhong Jin</name><email>pjincz@gmail.com</email></author><category term="zh" /><category term="wsl" /><summary type="html">这段时间一直在搞个unity webplayer的项目。尝试了各种组合方案，包括Windows+Linux虚拟机，Linux+unity测试版，Windows+Linux双机器。但无论怎么倒腾，都感觉难受。 正好前几天过十一，就顺便倒腾了一下传说中的WSL（Windows Subsystem for Linux），没想要甚是惊艳，就顺便写下来，分享分享，当个笔记。 如果实在无法避免双系统开发的话，那么WSL可能是目前最舒服的方案了。 WSL优点/缺点 要说WSL最大的优点的话，那么就是无缝了。Linux程序直接在Windows上，可以直接访问Windows的磁盘，和Windows共享端口池。要说用户体验，的确是秒杀虚拟机几条街。 要说缺点，如果作为一个开发系统，那么真的可以称得上完美了。如果非要找缺点，那么最大的缺点就是WSL继承了Windows的所有缺点：慢。但是我们毕竟不是只是拿来做开发么？ 安装/设置 WSL的安装不多说，微软自己的文档说的很详细，传送门在此。不过鉴于Windows的安装说明实在太过与罗嗦。其实就只有两步： 1：管理员方式打开PowerShell，然后运行 Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux 命令跑完了会提示要重启，跟着重启搞定了。 2：点击这里下载Ubuntu包，然后双击安装，根据提示设置账户，然后，一切搞定。 这才是极致的用户体验啊，给微软的同学们点10086个赞。。。。 修正默认文件夹创建权限问题 安装完了后，正常就会进入bash。但是这个时候，WSL默认的文件夹权限和原生的Linux不太一样（Linux默认创建文件夹为755权限，但是WSL默认创建文件夹为777）。这个会导致一系列的应用出问题，在进行下一步设置之前，必须要先处理这个问题。 解决这个问题不难，在~/.bashrc中加入： umask 022 修改完~/.bashrc后，使用. ~/.bashrc启用设置，或者直接关掉再重新打开ubuntu 清理PATH变量，避免干扰Linux程序运行 WSL默认情况下，会自动把Windows的环境变量也转换到Linux的PATH变量中。这是一个很酷的特征，例如你在bash中运行$ notepad.exe，然后记事本就会在Windows中被启用。但是如果你在Windows中装了cygwin、mingw或者nodejs之类的，就会产生各种互相干扰，导致脚本挂的莫名奇妙的。因此清理掉这些变量也是必要的。 这个问题也不难解决，在~/.bashrc中加入： export PATH=`echo $PATH | sed 's/:\/mnt\/c\/[^:]*//g'` 修改完~/.bashrc后，使用. ~/.bashrc启用设置，或者直接关掉再重新打开ubuntu 启用X11 然后就是跑个图形界面了，处理这个的时候，让我真的感受到了WSL的强大。因为实在是很简单。 本来刚开始是想在WSL上跑KDE的，搞了好久始终有问题，最后还是暂时不自虐了，弄个xfce4吧： $ sudo apt-get install xfce4 xfce4-terminal 然后安装Windows下的X11 Server，有几个不同的选择，试来试去还是VcXsrv最好用，一是开源免费，二是多顶层窗口的情况下可以正确的显示程序的图标。点击这里下载安装。 安装后启动VcXsrv时有一些选项，一路下一步就可以了。有个页面是问你四种启动方式： 如果想启动整个Linux桌面的话，可以选右下角那个，这样的话所有的Linux程序就像是运行在一个虚拟机里一样，可以把整个Linux桌面都启动起来。但是个人更喜欢默认的选项。这样的话，Linux的程序看起来更像是原生的Windows程序。 然后再在~/.bashrc中加入两个环境变量 export DISPLAY=:0 export LIBGL_ALWAYS_INDIRECT=1 修改完~/.bashrc后，使用. ~/.bashrc启用设置。 然后运行xfce4-terminal就可以启动xfce4的控制台了。如果想启动整个桌面玩的话，需要把上面截图中的选项设置为One Window模式，然后运行startxfce4。 修正中文显示问题 然后接下来会发现中午没法显示。这个简单，随便装个中文字体就可以。 sudo apt-get install fonts-wqy-* 修正中文输入法问题 接下里一个比较麻烦的问题是输入法。WSL下的程序是无法用Windows的输入法的。但是好消息是，可以装个Linux输入法。 sudo apt-get install fcitx fcitx-googlepinyin 安装完后，把需要的环境变量先丢入~/.bashrc export XMODIFIERS=@im=fcitx export GTK_IM_MODULE=fcitx export QT_IM_MODULE=fcitx 然后是再一次的应用配置，囧，. ~/.bashrc。接着启动输入法。 $ export $(dbus-launch) # 启动fcitx前必须先启动一个用户级的dbus $ fcitx &amp;amp; 这个时候输入法应该还是不能用，因为没有加入中文语言。打开输入法配置 $ fcitx-config-gtk3 并把googlepinyin设置进来。 如果你Win10系统有英文语言的话，按Win+Space切换到英语语言，然后按Ctrl+Space应该就能切出fcitx了。如果Win10只有中文的话，Ctrl+Space应该是会互相冲突的（我没试过），去系统里添加语言就可以了。 最后贴个图： 隐藏不想要的原始命令行窗口 写完整个文章后，总觉得好像缺了点啥，哈哈哈。原来是我太习惯RBTray了。都不觉得是个事了。 WSL会默认启动一个bash的窗口，这个窗口”明显的不好用“啊。否则折腾这么久，装个xfce4-terminal干啥呢？对吧。 干掉这个窗口有两个办法，方案1，换用setsid启动xfce4-terminal： setsid xfce4-terminal ^D # 按下Ctrl-d，退掉这个不想要的bash，这个时候xfce4-terminal就不会跟着退掉了 另一个方案，就是装一个RBTray，这伙真是个神器啊，只要几十k，所有不想要的窗口统统隐藏掉，最典型的就是vmware，免费版不允许headless，无所谓，直接隐藏掉。哈哈哈。 自动X11，输入法，系统服务 搞到这里，基本上的东西都可以使用了，只不过还是稍微有点繁琐，每次都需要先启动X11，再启动输入法，再启动一系列的服务。有的同学可能有洁癖，下面的可以直接忽略。 自动启动X11 启动XLauncher，选择喜好的配置后，一路走到最后一步。点击保存配置，保存到任意一个位置即可。 复制一下XLauncher的快捷方式，右键属性，打开设置，在目标后面追加：“ -run xluancher配置保存路径\config.xlaunch”。 退出现在的XLauncher，双击一下快捷方式，验证一下是不是和预期一致。 将快捷方式移入：C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp 打开任务管理器的startup面板可以再确认一下，That’s All。 自动启动输入法 自启动输入法稍微有点蛋疼，因为WSL不遵循标准的系统启动流程。网上有很多关于这方面的讨论，很多人建议用vb script解决这个问题。vbs这明显不是我的菜啊。而且即使是vbs方案，本身也算不上多漂亮。 本着实用主义至上原则，咱还是直接瞎搞吧： 打开~/.bashrc并加入： if ! pgrep fcitx &amp;gt; /dev/null; then # 如果没有找到fcitx的话，就启动个新的 export $(dbus-launch) fcitx &amp;amp; fi 自启动服务 然后作为开发，就是要启动各种稀奇古怪的东西了，例如mysql啥的。我个人觉得这个手工启动一下还是能接受的。毕竟也不是每次打开wsl都是想干后端的，对吧。但是如果这个都懒得动手的话，继续改造咱的bashrc即可。（反正都这么脏了，哈哈哈） 和输入法问题相比，有点区别的地方在于，启动服务需要su权限，这个也好说，直接google一下sudo nopassword自然一大把的解。 没了 搞完这几个步骤之后，基本上WSL就能像一个正常的Linux一样使用了。剩下的么，喜欢vim的装vim，喜欢emacs的装emacs。纯bash党装上tmuxinator。 WSL特性 IP地址共享 这个应该是WSL最让我吃惊的地方了，在WSL里的程序可以和Windows共享IP。因此如果在WSL下安装并启动了nginx的话，完全可以在Windows的浏览器里访问127.0.0.1。相当的爽。 磁盘访问 在Linux下访问Windows的磁盘非常的容易，全部挂载在/mnt下，例如/mnt/c就是c盘，/mnt/d就是d盘。 但是在Linux下访问Windows的磁盘有个问题，就是文件权限不太对，复制到Linux下全有可执行权限，这倒是一个相当烦的问题。 在Windows下访问Linux的磁盘也非常的容易，全部在C:\Users\&amp;lt;UserName&amp;gt;\AppData\Local\Packages\CanonicalGroupLimited.Ubuntu18.04onWindows_79rhkp1fndgsc\LocalState\rootfs下（取决于装的系统，这里是Ubuntu）。但是WSL官方强烈建议不要直接访问这个目录，因此还是别自找麻烦了。 磁盘权限 在WSL中，除了/mnt挂载的Windows磁盘，全部拥有Linux标准的权限，因此和Linux的兼容性很好。但是selinux之类的也是不要想太多了。 /mnt目录下挂载的Windows磁盘全部是Windows的权限。例如创建的文件的权限全是777。这个地方需要特别的小心，例如：不要混用Windows和Linux的git仓库，因为git在入库时会追溯文件的可执行权限。 colinux 用起WSL感觉真是棒的不行，但又不得不提起一个曾经叫colinux的项目。但只可惜colinux最终在32/64位系统交替中挂掉了。这些年一直是缅怀的不行啊。 没想到过了几年微软竟然自己投人，搞了这么个神器。真是峰回路转，哈哈哈。</summary></entry><entry><title type="html">是否存在一个函数，它的三阶导是函数本身</title><link href="https://mechanical-consciousness.com/2018/10/08/what-functions-third-devirative-equals-itself-zh.html" rel="alternate" type="text/html" title="是否存在一个函数，它的三阶导是函数本身" /><published>2018-10-08T00:00:00+08:00</published><updated>2018-10-08T00:00:00+08:00</updated><id>https://mechanical-consciousness.com/2018/10/08/what-functions-third-devirative-equals-itself-zh</id><content type="html" xml:base="https://mechanical-consciousness.com/2018/10/08/what-functions-third-devirative-equals-itself-zh.html">&lt;p&gt;“众所周之”，指数函数的求导是函数自身，而sin(x)的4阶求导（也就是求导四次的啦）是sin(x)本身，表示成数学公式就是：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;(e^x)' = e^x&lt;/script&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;(sin(x))^{(4)} = sin(x)&lt;/script&gt;

&lt;p&gt;一直很好奇，是否存在一个函数，它的二阶求导是函数本身。如果存在这么一个函数的话，是否还存在一个函数，它的三阶求导是函数本身呢？那如果推导到更广泛的n阶呢？&lt;/p&gt;

&lt;p&gt;本来只是无聊的好奇，没想到经过一顿搜索之后，竟然找到了超乎意料的漂亮的解，相当的意外，板砖过来给大家围观。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://math.stackexchange.com/questions/1080016/function-whose-third-derivative-is-itself&quot;&gt;参考资料传送门&lt;/a&gt;，
&lt;a href=&quot;#plot&quot;&gt;太长，懒得看传送门&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;什么函数的二阶求导是函数本身&quot;&gt;什么函数的二阶求导是函数本身&lt;/h3&gt;

&lt;p&gt;在研究探讨三阶求导是函数本身之前，先琢磨一下一个更简单的问题是有好处的。毕竟对于问题n=1和n=4都存在的情况下，n=2存在的可能性明显是比n=3存在的可能性要高很多的。&lt;/p&gt;

&lt;p&gt;首先，作为这个问题的趣味性，我们是肯定要先排除&lt;script type=&quot;math/tex&quot;&gt;e^x&lt;/script&gt;这个答案的。否则的话，这个问题就变得很无聊了，毕竟&lt;script type=&quot;math/tex&quot;&gt;e^x&lt;/script&gt;的导数就等于本身了，再给自己求n次导数都是自己本身。所以其实我们要找的是一个函数，使得：&lt;script type=&quot;math/tex&quot;&gt;f''(x) = f(x) 但 f'(x) \neq f(x)&lt;/script&gt;。&lt;/p&gt;

&lt;p&gt;这个其实不是那么困难，考虑一下：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;(e^{wx})' = w * e^{wx}&lt;/script&gt;

&lt;p&gt;再来一次：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;(e^{wx})'' = w^2 * e^{wx}&lt;/script&gt;

&lt;p&gt;其中，&lt;script type=&quot;math/tex&quot;&gt;e^{wx}&lt;/script&gt;部分就是原本的部分，那我们把w=-1，得到：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;(e^{-x})'' = (-e^{-x})' = e^{-x}&lt;/script&gt;

&lt;h3 id=&quot;什么函数的三阶求导是函数本身&quot;&gt;什么函数的三阶求导是函数本身&lt;/h3&gt;

&lt;p&gt;Cool，我们来到了我们真正感兴趣的问题了，我们先把已知的部分列一下：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;% &lt;![CDATA[
\begin{align}
\text{n = 1:}\quad &amp; e^x        \\
\text{n = 2:}\quad &amp; e^{-x}     \\
\text{n = 4:}\quad &amp; sin(x)     \\
\end{align} %]]&gt;&lt;/script&gt;

&lt;p&gt;其实问道到这里，线索已经非常的清晰明了了，就差大声喊出来了。但是可惜的是，我一直被sin/cos的所迷惑。如果我们把上面证明n=2的过程再哪来用一次：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;(e^{wx})^{(4)} = w^4 * e^{wx}&lt;/script&gt;

&lt;p&gt;&lt;script type=&quot;math/tex&quot;&gt;w^4 = 1&lt;/script&gt;？？这不就是&lt;script type=&quot;math/tex&quot;&gt;w = \pm1&lt;/script&gt;么？What？等等，回忆一下高中复数课程，w还有两个解，&lt;script type=&quot;math/tex&quot;&gt;w = \pm{i}&lt;/script&gt;。Bingo！我们把&lt;script type=&quot;math/tex&quot;&gt;w = i&lt;/script&gt;代进去：（当然，你要把w=-i带入进去也可以）&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;% &lt;![CDATA[
\begin{align}
(e^{ix})^{(4)} &amp; = i * (e^{ix})^{(3)} \\
               &amp; = i^2 * (e^{ix})'' \\
               &amp; = i^3 * (e^{ix})' \\
               &amp; = i^4 * e^{ix} \\
               &amp; = e^{ix}
   \end{align} %]]&gt;&lt;/script&gt;

&lt;p&gt;这个奇怪的&lt;script type=&quot;math/tex&quot;&gt;e^{ix}&lt;/script&gt;到底和&lt;script type=&quot;math/tex&quot;&gt;sin(x)&lt;/script&gt;什么关系？是时候该拿出理科生骗文科妹子专用公式（欧拉公式）了：（可惜我是工科生，23333）&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;e^{ix} = cos(x) + isin(x)&lt;/script&gt;

&lt;p&gt;然而，我们这里并不是要欧拉公式，我们把欧拉公式变形一下：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;sin(x) = \frac{e^{ix} - e^{-ix}}{2}&lt;/script&gt;

&lt;p&gt;根据“最最基础的求导公式”，我们知道：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;(w * f)' = w * f',\quad (w * f)^{(n)} = w * f^{(n)}&lt;/script&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;(f + g)' = f' + g',\quad (f + g)^{(n)} = f^{(n)} + g^{(n)}&lt;/script&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;sin^{(4)}(x) = (\frac{e^{ix} - e^{-ix}}{2})^{(4)} = \frac{(e^{ix})^{(4)} - (e^{-ix})^{(4)}}{2} = \frac{e^{ix} - e^{-ix}}{2} = sin(x)&lt;/script&gt;

&lt;p&gt;综上所知：sin(x)的四阶求导等于sin(x)证毕。&lt;/p&gt;

&lt;p&gt;等等等等，WTF？这TM不是脱裤子放屁么？好吧好吧，前面的请无视，咱重新来：&lt;/p&gt;

&lt;h3 id=&quot;什么函数的三阶求导是函数本身真&quot;&gt;什么函数的三阶求导是函数本身（真）&lt;/h3&gt;

&lt;p&gt;其实问题到这里，大家也都知道要怎么做了，非常的简单，答案就是：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;f(x) = e^{wx}, \quad 其中w^3=1&lt;/script&gt;

&lt;p&gt;我们按照”高中知识”知道，w有3个解，分别是：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;% &lt;![CDATA[
\begin{align}
w_1 &amp; = 1                                           \\
w_2 &amp; = cos(\frac{2}{3}\pi) + isin(\frac{2}{3}\pi)  \\
w_3 &amp; = cos(\frac{2}{3}\pi) - isin(\frac{2}{3}\pi)  \\
   \end{align} %]]&gt;&lt;/script&gt;

&lt;p&gt;也就是说，我们至少至少已经知道了两个函数，这两个函数的3阶导是自身：（排除&lt;script type=&quot;math/tex&quot;&gt;e^x&lt;/script&gt;）&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;\begin{align}
f_1(x) = e^{cos(\frac{2}{3}\pi) + isin(\frac{2}{3}\pi)} \\
f_2(x) = e^{cos(\frac{2}{3}\pi) - isin(\frac{2}{3}\pi)}
   \end{align}&lt;/script&gt;

&lt;p&gt;问题到这里貌似差不多了，但我们出发的初衷是为了找个实域上的函数啊，不是想找个复平面上的啊。这不，明显又跑题了啊。但是参考资料到这里也就到头了，但咱必须继续：&lt;/p&gt;

&lt;p&gt;不得不说上面的&lt;script type=&quot;math/tex&quot;&gt;w_2&lt;/script&gt;，&lt;script type=&quot;math/tex&quot;&gt;w_3&lt;/script&gt;写起来太累了，为了下面的证明舒服点，我们先：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;a = cos(\frac{2}{3}\pi) \\
   b = sin(\frac{2}{3}\pi)&lt;/script&gt;

&lt;p&gt;OK，这个时候：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;% &lt;![CDATA[
\begin{align}
   w_2 &amp; = a + bi \\
   w_3 &amp; = a - bi \\
   f_1(x) &amp; = e^{(a+bi)x} \\
   f_2(x) &amp; = e^{(a-bi)x} \\
   \end{align} %]]&gt;&lt;/script&gt;

&lt;p&gt;然后呢：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;% &lt;![CDATA[
\begin{align}
   f(x) &amp; = f_1(x) + f_2(x)                                         \\
        &amp; = e^{(a+bi)x} + e^{(a-bi)x}                               \\
        &amp; = e^{ax} * e^{bix} + e^{ax} * e^{-bix}                    \\
        &amp; = e^{ax} * (e^{bix} + e^{-bix})                           \\
        &amp; = e^{ax} * (cos(bx) + isin(bx) + cos(-bx) + isin(-bx))    \\
        &amp; = e^{ax} * (2 * cos(bx))                                  \\
        &amp; = 2 * e^{ax} * cos(bx)                                    \\
        &amp; = 2 * e^{cos(\frac{2}{3}\pi)x} * cos(sin(\frac{2}{3}\pi)x)
   \end{align} %]]&gt;&lt;/script&gt;

&lt;p&gt;向上面的sin(x)和cos(x)学习一下，我们给这个函数/2，得到我们最终想要的g(x)：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;g(x) = e^{cos(\frac{2}{3}\pi)x} * cos(sin(\frac{2}{3}\pi)x)&lt;/script&gt;

&lt;p&gt;验证这个答案的正确性，着实不是一件容易的事情啊。生命苦短，我们还是直接祭出神器来吧：
&lt;a href=&quot;https://www.derivative-calculator.net/#expr=e%5E%28cos%282%2F3%2Api%29%2Ax%29%2Acos%28sin%282%2F3%2Api%29%2Ax%29&amp;amp;difforder=3&quot;&gt;derivative-calculator.net&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;来来来，最后来瞅瞅我们辛辛苦苦找半天的函数长啥样的：&lt;/p&gt;

&lt;h4 id=&quot;plot&quot;&gt;plot&lt;/h4&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;f(x) = e^{cos(\frac{2}{3}\pi)x} * cos(sin(\frac{2}{3}\pi)x)&lt;/script&gt;

&lt;p&gt;&lt;img src=&quot;/assets/posts/what-functions-third-devirative-equals-itself-zh/plot.png&quot; alt=&quot;plog&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;既往开来&quot;&gt;既往开来&lt;/h3&gt;

&lt;p&gt;一直以来，数学家们就是一群追（自）求（找）自（麻）我（烦）的人。找到了n=3的，自然不能止步于此，n=5呢？n=6呢？这个还真不难，至少以下函数肯定是对的：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;f(x) = e^{cos(\frac{2\pi}{n})x} * cos(sin(\frac{2\pi}{n})x)&lt;/script&gt;

&lt;p&gt;顺便带入一下n=1，2，4试试：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;f(x)_{n=1} = e^{cos(2\pi)x} * cos(sin(2\pi)x) = e^x * cos(0) = e^x&lt;/script&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;f(x)_{n=2} = e^{cos(\pi)x} * cos(sin(\pi)x) = e^{-x} * cos(0) = e^{-x}&lt;/script&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;f(x)_{n=4} = e^{cos({\pi \over 2})x} * cos(sin({\pi \over 2})x) = e^0 * cos(x) = cos(x)&lt;/script&gt;

&lt;p&gt;不错吧？哈哈。那么能不能列出所有所有的解呢？哈哈哈，貌似是可以的，但是这个着实超出本人的数学水平太多了。如果我没猜错的话，全解应该是&lt;script type=&quot;math/tex&quot;&gt;e^{ {w_i}x}&lt;/script&gt;的全部线性组合。&lt;/p&gt;

&lt;p&gt;但其实这个问题另一个更加有趣的扩展方向是：如果我们给n=2.5会怎么样？以及给sin(x)求0.5次导会怎样？&lt;/p&gt;

&lt;p&gt;另外一个很好玩的话题是，证明中“路过”了复数，虽然整个事情看起来和复数是没半毛钱关系的。&lt;/p&gt;

&lt;p&gt;哈哈哈，先扯到这里，等有空了接着扯。。。&lt;/p&gt;</content><author><name>Chizhong Jin</name><email>pjincz@gmail.com</email></author><category term="zh" /><category term="math" /><summary type="html">“众所周之”，指数函数的求导是函数自身，而sin(x)的4阶求导（也就是求导四次的啦）是sin(x)本身，表示成数学公式就是： 一直很好奇，是否存在一个函数，它的二阶求导是函数本身。如果存在这么一个函数的话，是否还存在一个函数，它的三阶求导是函数本身呢？那如果推导到更广泛的n阶呢？ 本来只是无聊的好奇，没想到经过一顿搜索之后，竟然找到了超乎意料的漂亮的解，相当的意外，板砖过来给大家围观。 参考资料传送门， 太长，懒得看传送门 什么函数的二阶求导是函数本身 在研究探讨三阶求导是函数本身之前，先琢磨一下一个更简单的问题是有好处的。毕竟对于问题n=1和n=4都存在的情况下，n=2存在的可能性明显是比n=3存在的可能性要高很多的。 首先，作为这个问题的趣味性，我们是肯定要先排除这个答案的。否则的话，这个问题就变得很无聊了，毕竟的导数就等于本身了，再给自己求n次导数都是自己本身。所以其实我们要找的是一个函数，使得：。 这个其实不是那么困难，考虑一下： 再来一次： 其中，部分就是原本的部分，那我们把w=-1，得到： 什么函数的三阶求导是函数本身 Cool，我们来到了我们真正感兴趣的问题了，我们先把已知的部分列一下： 其实问道到这里，线索已经非常的清晰明了了，就差大声喊出来了。但是可惜的是，我一直被sin/cos的所迷惑。如果我们把上面证明n=2的过程再哪来用一次： ？？这不就是么？What？等等，回忆一下高中复数课程，w还有两个解，。Bingo！我们把代进去：（当然，你要把w=-i带入进去也可以） 这个奇怪的到底和什么关系？是时候该拿出理科生骗文科妹子专用公式（欧拉公式）了：（可惜我是工科生，23333） 然而，我们这里并不是要欧拉公式，我们把欧拉公式变形一下： 根据“最最基础的求导公式”，我们知道： So: 综上所知：sin(x)的四阶求导等于sin(x)证毕。 等等等等，WTF？这TM不是脱裤子放屁么？好吧好吧，前面的请无视，咱重新来： 什么函数的三阶求导是函数本身（真） 其实问题到这里，大家也都知道要怎么做了，非常的简单，答案就是： 我们按照”高中知识”知道，w有3个解，分别是： 也就是说，我们至少至少已经知道了两个函数，这两个函数的3阶导是自身：（排除） 问题到这里貌似差不多了，但我们出发的初衷是为了找个实域上的函数啊，不是想找个复平面上的啊。这不，明显又跑题了啊。但是参考资料到这里也就到头了，但咱必须继续： 不得不说上面的，写起来太累了，为了下面的证明舒服点，我们先： OK，这个时候： 然后呢： 向上面的sin(x)和cos(x)学习一下，我们给这个函数/2，得到我们最终想要的g(x)： 验证这个答案的正确性，着实不是一件容易的事情啊。生命苦短，我们还是直接祭出神器来吧： derivative-calculator.net 来来来，最后来瞅瞅我们辛辛苦苦找半天的函数长啥样的： plot 既往开来 一直以来，数学家们就是一群追（自）求（找）自（麻）我（烦）的人。找到了n=3的，自然不能止步于此，n=5呢？n=6呢？这个还真不难，至少以下函数肯定是对的： 顺便带入一下n=1，2，4试试： 不错吧？哈哈。那么能不能列出所有所有的解呢？哈哈哈，貌似是可以的，但是这个着实超出本人的数学水平太多了。如果我没猜错的话，全解应该是的全部线性组合。 但其实这个问题另一个更加有趣的扩展方向是：如果我们给n=2.5会怎么样？以及给sin(x)求0.5次导会怎样？ 另外一个很好玩的话题是，证明中“路过”了复数，虽然整个事情看起来和复数是没半毛钱关系的。 哈哈哈，先扯到这里，等有空了接着扯。。。</summary></entry><entry><title type="html">如何制作一个”好用”的音量控制滑槽</title><link href="https://mechanical-consciousness.com/2018/09/29/how-to-make-a-good-volume-slider-zh.html" rel="alternate" type="text/html" title="如何制作一个&quot;好用&quot;的音量控制滑槽" /><published>2018-09-29T00:00:00+08:00</published><updated>2018-09-29T00:00:00+08:00</updated><id>https://mechanical-consciousness.com/2018/09/29/how-to-make-a-good-volume-slider-zh</id><content type="html" xml:base="https://mechanical-consciousness.com/2018/09/29/how-to-make-a-good-volume-slider-zh.html">&lt;p&gt;音量控制滑槽啥的，我相信对每个“经验丰富”的程序员来说都是分分钟的事情。但是我今天为啥要拿出来谈这个事情呢？因为再小的东西都有细节。而且吧，还一定会有人踩进去。&lt;/p&gt;

&lt;p&gt;相信同学们一定一定有过这种体验：到了晚上，音量拖到最小的一格仍然太大，再往左一拖就静音了。白天吧，音量区右边的一大半都没太大区别。&lt;/p&gt;

&lt;h3 id=&quot;线性空间对数空间&quot;&gt;线性空间/对数空间&lt;/h3&gt;

&lt;p&gt;为啥会有这样的问题：人对音量的感知的范围非常的大，通常上来说一个正常人的耳朵在 20dB - 90dB 之间的范围都处于“比较舒适”的状态，但是这个范围足足有&lt;script type=&quot;math/tex&quot;&gt;10^\frac{90-20}{20} = 3162&lt;/script&gt;倍。如果再算上不太舒适区域的话，上个十万倍都是轻轻松松的。&lt;/p&gt;

&lt;p&gt;但很多人写程序的时候并没有去考虑这么多，往往直接把这么大的空间直接线性的映射到音量控制滑槽上。导致的直接结果就是：音量大的区域，拉半天也没啥变化，音量小的区域，稍微一拖就没了。&lt;/p&gt;

&lt;p&gt;那这个问题怎么办？其实只要简单的音量进行对数化处理就好了，废话不多说，直接上代码：&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;#include &amp;lt;math.h&amp;gt;
&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;VOLUME_SLIDER_MIN&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;VOLUME_SLIDER_MAX&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;VOLUME_MIN&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mo&quot;&gt;005&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;VOLUME_MAX&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 将滑槽块位置换算为音量系数&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;sliderPosToVolume&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 特殊处理，最左侧一格表示静音&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;VOLUME_SLIDER_MIN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 计算对数化区间，（这不是个性能很敏感的函数，所以就直接丢这里了）&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;l_min&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;VOLUME_MIN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;l_max&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;VOLUME_MAX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 在对数区间线性插值&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;l_vol&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;l_max&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;l_min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;VOLUME_SLIDER_MAX&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;VOLUME_SLIDER_MIN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;l_min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 把对数空间转换转换回线性空间&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;exp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;l_val&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 将音量系数换算为滑槽位置&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;volumeToSliderPos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vol&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 特殊处理0值&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vol&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;VOLUME_MIN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 计算对数化区间和数值&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;l_min&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;VOLUME_MIN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;l_max&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;VOLUME_MAX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;l_vol&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vol&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// 计算滑块位置&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;l_vol&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;l_min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;l_max&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;l_min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                                &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;VOLUME_SLIDER_MAX&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;VOLUME_SLIDER_MIN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;VOLUME_SLIDER_MIN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 裁剪输出&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;VOLUME_SLIDER_MAX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;VOLUME_SLIDER_MAX&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 四舍五入并返回&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;roundf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pos&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;上面的代码其实非常的简单，我相信不需要多解释。唯一特殊注意就是0值。在对数空间里是没有0值的，但是现实世界里却是有静音的，因此需要特殊的处理。&lt;/p&gt;

&lt;h3 id=&quot;拿对数化计算05的阶乘05&quot;&gt;拿对数化计算0.5的阶乘（0.5!）&lt;/h3&gt;

&lt;p&gt;其实对数化是一个非常非常好玩又好用的东西。当很多问题数值非常大或非常小或者变化区间非常大时，丢一个对数化下去经常会有惊喜。&lt;/p&gt;

&lt;p&gt;例如很多人对&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n!&lt;/code&gt;非常的感兴趣（哈哈哈，其实说的就是我自己啦）。但是真要研究琢磨一下的，完全没法下手。只知道增长的很快很快。完全没法画图。但如果拍一个log下去：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;log(n!) = log(1) + log(2) + log(3) + ... + log(n)&lt;/script&gt;

&lt;p&gt;其实对数化了之后，明显可以看出log(n!)在一个蛮大的区间里是接近线性的。是不是很有惊喜？&lt;/p&gt;

&lt;p&gt;我们知道&lt;script type=&quot;math/tex&quot;&gt;n! = (n-1)! * n&lt;/script&gt;，那么我们是不是也可以假设&lt;script type=&quot;math/tex&quot;&gt;1.5! = 0.5! * 1.5&lt;/script&gt;呢？那么把这个反过来：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;0.5! = \frac{1.5!}{1.5} = \frac{2.5!}{2.5 * 1.5} = \frac{1000.5!}{999.5 * 998.5 * ... * 1.5}&lt;/script&gt;

&lt;p&gt;然后呢，由于在n很大的时候，log(n!)是接近线性的，OK！瞬间有思路了，来来来，我们也走一个。（js代码，不想写c自虐了，哈哈哈）&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 先写个函数计算log(n!)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;log_fact&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lf&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;lf&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 恩恩，取个很大n，使得空间尽可能线性&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lf_10000&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;log_fact&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lf_10001&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;log_fact&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 线性插值，计算log(10000.5!)的近似值&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lf_10000_5&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lf_10000&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lf_10001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 一步一步减去10000.5, 9999.5, 9998.5, ....&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lf_0_5&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;lf_10000_5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;lf_0_5&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 从对数空间换算回指数空间&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;fact_0_5&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Math&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;exp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;lf_0_5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fact_0_5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;嗯嗯，我们得到个结果&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.886238002222443&lt;/code&gt;，看起来似乎没啥特殊的哈，我们给这个数值平方一下，再乘以4：&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;fact_0_5&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3.1416711863329074&lt;/code&gt;！咋样，吓一跳吧，这不就是PI么，哈哈哈哈。其实历史上第一个计算&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.5!&lt;/code&gt;的人还真就是这么干的：就是先把&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n!&lt;/code&gt;对数化再进行拟合插值，再换算回去。下面这个图就是插值完的函数（实际上是log((n-1)!)）&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/posts/how-to-make-a-good-volume-slider-zh/log_gamma.gif&quot; alt=&quot;log gamma&quot; /&gt;&lt;/p&gt;

&lt;p&gt;把我们刚刚计算&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.5!&lt;/code&gt;的公式展开的话，又会有新的好玩的东西：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;0.5! = { 1000.5! \over 1000.5 * 999.5 * 998.5 * 997.5 * ... * 1.5 }&lt;/script&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;1000.5! \approx e^{log(1000!) + log(1001!) \over 2} = 1000! * \sqrt{1001}&lt;/script&gt;

&lt;p&gt;组合一下上面两条公式，我们得到：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;% &lt;![CDATA[
\begin{align}
0.5! &amp; \approx \sqrt{1001} * {1000 * 999 * 998 * ... * 1 \over 1000.5 * 999.5 * 998.5 * ... * 1.5} \\
     &amp; = \sqrt{1001} * {2000 * 1998 * 1996 * ... * 2 \over 2001 * 1999 * 1997 * ... * 3} \\
(0.5!)^2 * 2 &amp; \approx \sqrt{1001}^2 * 2 * {2000^2 * 1998^2 * 1996^2 * ... * 2^2 \over 2001^2 * 1999^2 * 1997^2 * ... * 3^2 } \\
             &amp; = {2002 * 2000 * 2000 * 1998 * 1998 * 1996 * ... * 4 * 4 * 2 \over 2001 * 2001 * 1999 * 1999 * 1997 * 1997 * ... * 5 * 3 * 3} * \frac{2}{1} \\
             &amp; = \frac{2}{1} * \frac{2}{3} * \frac{4}{3} * \frac{4}{5} * ... * \frac{2000}{1999} * \frac{2000}{2001} * \frac{2002}{2001}
\end{align} %]]&gt;&lt;/script&gt;

&lt;p&gt;非常不负责任的把&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n&lt;/code&gt;推向无穷大后有：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;(0.5!)^2 * 2 = \frac{2}{1} * \frac{2}{3} * \frac{4}{3} * \frac{4}{5} * \frac{6}{5} * \frac{6}{7} * \frac{8}{7} ...&lt;/script&gt;

&lt;p&gt;由于前面知道&lt;script type=&quot;math/tex&quot;&gt;0.5! = \frac{\sqrt{\pi}}{2}&lt;/script&gt;，所以：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;(0.5!)^2 * 2 = \frac{\pi}{2} = \frac{2}{1} * \frac{2}{3} * \frac{4}{3} * \frac{4}{5} * \frac{6}{5} * \frac{6}{7} * \frac{8}{7} ...&lt;/script&gt;

&lt;p&gt;哇啊啊啊啊啊，这就是传说中的“沃利斯公式”啊。哈哈哈，当然，我这里纯属瞎证明哈。其实在历史上是恰好反过来，用“沃利斯公式”证明&lt;script type=&quot;math/tex&quot;&gt;0.5! = \frac{\sqrt{\pi}}{2}&lt;/script&gt;的。&lt;/p&gt;

&lt;h3 id=&quot;对数化处理贝叶斯分类器&quot;&gt;对数化处理贝叶斯分类器&lt;/h3&gt;

&lt;p&gt;再一个边写文章就边随手想到的例子就是贝叶斯分类器了，相信不少人手摞代码的时候都被前面的0给绕晕了。如果参数集再大一些，最后浮点数全0是非常正常的现象。再拍一个log下去：&lt;/p&gt;

&lt;script type=&quot;math/tex; mode=display&quot;&gt;% &lt;![CDATA[
\begin{align}
p &amp;  = p_1 * p_2 * p_3 * p_4 * ... * p_n \\
log(p) &amp; = log(p_1) + log(p_2) + log(p_3) + log(p_4) + ... + log(p_n)
   \end{align} %]]&gt;&lt;/script&gt;

&lt;p&gt;大部分的时候，用贝叶斯只是为了找出最大的，连换算回去的步骤都省了。另一个好处是，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;log&lt;/code&gt;拍下去之后都可以用矩阵运算，还可以并行了，23333。再眼尖点的，还会发现对数完后的贝叶斯怎么和logistic有点像，哈哈哈哈。&lt;/p&gt;

&lt;p&gt;哈哈哈哈，不能再说了，再说就要暴露自己几斤几两了。。。。其实我本来只不过想吐嘈一下某某播放器晚上“有点吵”而已，:’)&lt;/p&gt;</content><author><name>Chizhong Jin</name><email>pjincz@gmail.com</email></author><category term="zh" /><category term="c/c++" /><category term="math" /><summary type="html">音量控制滑槽啥的，我相信对每个“经验丰富”的程序员来说都是分分钟的事情。但是我今天为啥要拿出来谈这个事情呢？因为再小的东西都有细节。而且吧，还一定会有人踩进去。 相信同学们一定一定有过这种体验：到了晚上，音量拖到最小的一格仍然太大，再往左一拖就静音了。白天吧，音量区右边的一大半都没太大区别。 线性空间/对数空间 为啥会有这样的问题：人对音量的感知的范围非常的大，通常上来说一个正常人的耳朵在 20dB - 90dB 之间的范围都处于“比较舒适”的状态，但是这个范围足足有倍。如果再算上不太舒适区域的话，上个十万倍都是轻轻松松的。 但很多人写程序的时候并没有去考虑这么多，往往直接把这么大的空间直接线性的映射到音量控制滑槽上。导致的直接结果就是：音量大的区域，拉半天也没啥变化，音量小的区域，稍微一拖就没了。 那这个问题怎么办？其实只要简单的音量进行对数化处理就好了，废话不多说，直接上代码： #include &amp;lt;math.h&amp;gt; static const int VOLUME_SLIDER_MIN = 1; static const int VOLUME_SLIDER_MAX = 30; static const float VOLUME_MIN = 0.005; static const float VOLUME_MAX = 1; // 将滑槽块位置换算为音量系数 float sliderPosToVolume(int pos) { // 特殊处理，最左侧一格表示静音 if (pos &amp;lt; VOLUME_SLIDER_MIN) return 0; // 计算对数化区间，（这不是个性能很敏感的函数，所以就直接丢这里了） float l_min = log(VOLUME_MIN); float l_max = log(VOLUME_MAX); // 在对数区间线性插值 float l_vol = (l_max - l_min) / (VOLUME_SLIDER_MAX - VOLUME_SLIDER_MIN) * pos + l_min; // 把对数空间转换转换回线性空间 return exp(l_val); } // 将音量系数换算为滑槽位置 int volumeToSliderPos(float vol) { // 特殊处理0值 if (vol &amp;lt; VOLUME_MIN) return 0; // 计算对数化区间和数值 float l_min = log(VOLUME_MIN); float l_max = log(VOLUME_MAX); float l_vol = log(vol); // 计算滑块位置 float pos = (l_vol - l_min) / (l_max - l_min) * (VOLUME_SLIDER_MAX - VOLUME_SLIDER_MIN) + VOLUME_SLIDER_MIN; // 裁剪输出 if (pos &amp;gt; VOLUME_SLIDER_MAX) pos = VOLUME_SLIDER_MAX; // 四舍五入并返回 return (int)roundf(pos); } 上面的代码其实非常的简单，我相信不需要多解释。唯一特殊注意就是0值。在对数空间里是没有0值的，但是现实世界里却是有静音的，因此需要特殊的处理。 拿对数化计算0.5的阶乘（0.5!） 其实对数化是一个非常非常好玩又好用的东西。当很多问题数值非常大或非常小或者变化区间非常大时，丢一个对数化下去经常会有惊喜。 例如很多人对n!非常的感兴趣（哈哈哈，其实说的就是我自己啦）。但是真要研究琢磨一下的，完全没法下手。只知道增长的很快很快。完全没法画图。但如果拍一个log下去： 其实对数化了之后，明显可以看出log(n!)在一个蛮大的区间里是接近线性的。是不是很有惊喜？ 我们知道，那么我们是不是也可以假设呢？那么把这个反过来： 然后呢，由于在n很大的时候，log(n!)是接近线性的，OK！瞬间有思路了，来来来，我们也走一个。（js代码，不想写c自虐了，哈哈哈） // 先写个函数计算log(n!) function log_fact(n) { var lf = 0; for (var i = 1; i &amp;lt;= n; ++i) { lf += Math.log(i); } return lf; } // 恩恩，取个很大n，使得空间尽可能线性 var lf_10000 = log_fact(10000); var lf_10001 = log_fact(10001); // 线性插值，计算log(10000.5!)的近似值 var lf_10000_5 = (lf_10000 + lf_10001) / 2; // 一步一步减去10000.5, 9999.5, 9998.5, .... var lf_0_5 = lf_10000_5; for (var i = 10000; i &amp;gt;= 1; --i) { lf_0_5 -= Math.log(i + 0.5); } // 从对数空间换算回指数空间 var fact_0_5 = Math.exp(lf_0_5); console.log(fact_0_5); 嗯嗯，我们得到个结果0.886238002222443，看起来似乎没啥特殊的哈，我们给这个数值平方一下，再乘以4： console.log((fact_0_5 * 2) ** 2); 3.1416711863329074！咋样，吓一跳吧，这不就是PI么，哈哈哈哈。其实历史上第一个计算0.5!的人还真就是这么干的：就是先把n!对数化再进行拟合插值，再换算回去。下面这个图就是插值完的函数（实际上是log((n-1)!)） 把我们刚刚计算0.5!的公式展开的话，又会有新的好玩的东西： 组合一下上面两条公式，我们得到： 非常不负责任的把n推向无穷大后有： 由于前面知道，所以： 哇啊啊啊啊啊，这就是传说中的“沃利斯公式”啊。哈哈哈，当然，我这里纯属瞎证明哈。其实在历史上是恰好反过来，用“沃利斯公式”证明的。 对数化处理贝叶斯分类器 再一个边写文章就边随手想到的例子就是贝叶斯分类器了，相信不少人手摞代码的时候都被前面的0给绕晕了。如果参数集再大一些，最后浮点数全0是非常正常的现象。再拍一个log下去： 大部分的时候，用贝叶斯只是为了找出最大的，连换算回去的步骤都省了。另一个好处是，log拍下去之后都可以用矩阵运算，还可以并行了，23333。再眼尖点的，还会发现对数完后的贝叶斯怎么和logistic有点像，哈哈哈哈。 哈哈哈哈，不能再说了，再说就要暴露自己几斤几两了。。。。其实我本来只不过想吐嘈一下某某播放器晚上“有点吵”而已，:’)</summary></entry><entry><title type="html">从include到require - 论node require设计</title><link href="https://mechanical-consciousness.com/2018/09/23/from-include-to-require-zh.html" rel="alternate" type="text/html" title="从include到require - 论node require设计" /><published>2018-09-23T00:00:00+08:00</published><updated>2018-09-23T00:00:00+08:00</updated><id>https://mechanical-consciousness.com/2018/09/23/from-include-to-require-zh</id><content type="html" xml:base="https://mechanical-consciousness.com/2018/09/23/from-include-to-require-zh.html">&lt;p&gt;由于“工作”需要，在上周的某个月黑风高的夜晚，“新建”了一个vue项目。当然了，像我这种不做死就会死星人，自然“一不小心”就把webpack升级到了3.x。&lt;/p&gt;

&lt;p&gt;然后惊喜的发现&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;var Vue = require('Vue');&lt;/code&gt;竟然不工作了？？？然后再发现&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;var MyComponent = require('./MyComponent');&lt;/code&gt;竟然也不工作了？？？&lt;/p&gt;

&lt;p&gt;然后就是一路的Google，WHY??? 回头换来一个说法竟然是为了“全方位”的兼容import/export，从babel xxx版本开始，require必须后面跟个.default才能导入export的结果。&lt;/p&gt;

&lt;p&gt;我擦，我这是瞬间要炸毛的节奏啊。以牺牲require用户体验的方式，强制“退休”require。你们真的“赢了”。&lt;/p&gt;

&lt;p&gt;本来吧，es标准委员会推出esModule标准的时候，就想吐嘈这完全是个开历史倒车的标准。但是本着“和谐共处 x项原则”。你推你的import，我用我的require，各走阳光道。但是这次的事件真的是着实恶心到我了。&lt;/p&gt;

&lt;p&gt;本来想写篇文章喷import，但是想想作为一个老年人，还是火气还是不要这么大，与其互喷，还不如写点文章，来聊聊require是如何走到今天的吧。相信各位看官看完，自然对import是进车还是倒车就有了自己的看法了。&lt;/p&gt;

&lt;h3 id=&quot;1-从cc-include说起&quot;&gt;1. 从c/c++ include说起&lt;/h3&gt;

&lt;p&gt;在“上古时代”，文件系统啥的都不存在，自然也不存在什么include/import/require的了。在那个年代，一切的“函数”都被编译好了，安安静静的躺在寻址空间的某个角落。&lt;/p&gt;

&lt;p&gt;当我们需要某个“函数”的时候，我们只需要说：“啊，我知道一个人叫张三，长的人高马大，一米六八。”就可以了。&lt;/p&gt;

&lt;p&gt;换成c的代码就是：&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;char&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fmt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这就是所谓的“声明”。很多刚学c的同学很是难以理解，声明和定义区别在哪里。这个其实蛮正常的啦，就像是生活在现代社会的人，难以很多“传统”习俗都是蛮难理解的。(就像是为什么要喝热水，手工狗脸)&lt;/p&gt;

&lt;p&gt;但是人嘛，都是懒得，程序员那是尤其的懒。一遍一遍的写累是一方面，更是容易出错。我相信眼尖的同学已经发现问题了：为什么fmt的类型是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;char *&lt;/code&gt;而不是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;const char *&lt;/code&gt;？因为：我就是故意的啊。。。&lt;/p&gt;

&lt;p&gt;眼睛更尖同学应该还会注意到&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;printf&lt;/code&gt;的返回值，对的，printf应该返回&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int&lt;/code&gt;，而不是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;void&lt;/code&gt;。来源于那个年代奇怪的信仰。如果不是对着手册抄的话，我估计现在多数人都会弄错。&lt;/p&gt;

&lt;p&gt;那这个错的离谱的代码能跑吗？答案是“能”！这个明显的不合理啊，这简直不是自欺欺人么？？？显然，我们的“祖先们”也发现了这个问题。在原始朴素的想法驱动下，那自然就是把一部分重复的东西提取出来，include自然也就出现了。&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;#include &quot;那些我懒得重复写的声明.h&quot;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;显然的，这种朴素的include和现代的包管理有着天壤之别。inlcude赋予程序员灵活性的同时，也带来了一堆苦恼。相信下面代码是个c/c++的程序员都写过：&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;#ifndef __MY_PROJECT_MY_WIDGET_H__
#define __MY_PROJECT_MY_WIDGET_H__
&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;

&lt;span class=&quot;cp&quot;&gt;#endif//__MY_PROJECT_MY_WIDGET_H__
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在历史上，头文件里是只有函数声明的，在c语言里你对同一个函数声明1次或者声明100次，编译器完全都是“无动于衷”，自然也不会有防重复导入的功能了。甚至不少c/c++的代码还依赖于这种重复导入的功能。&lt;/p&gt;

&lt;p&gt;再一个典型的例子就是include的顺序问题，相信写过c++的人，对include hell啥的都是不陌生的。&lt;/p&gt;

&lt;p&gt;include只是一个萌芽，问题和坑几乎漫山遍野，程序猿从来都是解决一个问题时创造两个问题生物。做包管理肯定也不例外。include虽然带来了漫山遍野的问题，但include解决了当年的核心诉求。我们在这里就不细数include的问题了，不公正也没必要。&lt;/p&gt;

&lt;h3 id=&quot;2-从cc-include到python-import&quot;&gt;2. 从c/c++ include到python import&lt;/h3&gt;

&lt;p&gt;从c/c++ include到python或者java的import可以说是编程历史上的一个重大进步。个人没有花时间去追溯到底是python借鉴了java，还是java借鉴了python，也或许它们共同的受到了另一个语言的启发。在这里之所以以python为例，是因为es的import是以python import为原型设计的。&lt;/p&gt;

&lt;p&gt;但不论如何，import出现解决了大量的问题。&lt;/p&gt;

&lt;p&gt;例如你不需要在写一个独立的头文件，因为本质上来说，头文件完全是可以通过源文件生成的。&lt;/p&gt;

&lt;p&gt;再例如就是你不再需要担心文件之间的互相引用关系，也不需要再担心文件的引用顺序，文件的防重复引用。&lt;/p&gt;

&lt;p&gt;甚至python也许有意也许无意下解决了一个c/c++的痛点，就是名字空间问题：假如村子里有两个叫王麻子的怎么办？&lt;/p&gt;

&lt;p&gt;在c/c++年代，大家普遍的使用前缀来解决这个问题：既然村子里有两个王麻子，这个事情好办，一个取名成&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;村头的王麻子&lt;/code&gt;，另一个取名为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;村尾的王麻子&lt;/code&gt;问题就解决了。&lt;/p&gt;

&lt;p&gt;当然了，如果你要把一个镇子的人都拉下来一起写代码，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;王麻子们&lt;/code&gt;自然就不能这么取名字了，自然就出现了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;里岙村村头王麻子&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;外山村村尾王麻子&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;这个看起来貌似没什么的问题，当随着越来越多的程序员参与开发后，问题越来越爆炸。最后发展到什么样子呢？我们来看一个gtk的函数命名：&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;gtk_menu_tool_button_set_arrow_tooltip_markup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个不是我特意挑特意选的，而是我随手打开gtk的手册挑了一个，这个函数命名足足有45个字符。这显然写起来不是那么舒服。&lt;/p&gt;

&lt;p&gt;但即使是这样，名字冲突还是层出不穷。最典型的就是c/c++里面那些让人崩溃的“基本拓展类型”，比如啥&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DWORD&lt;/code&gt;啊，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UINT32&lt;/code&gt;啊，到处都在定义，还都不太一致。一出错完全不知道引了哪个定义。&lt;/p&gt;

&lt;p&gt;我为什么说我不知道python到底是有意还是无意解决了这个问题呢？&lt;/p&gt;

&lt;p&gt;在python的import语法中，当你导入一个包时，包里面的东西是不会展开到你的名字空间的。而是会有一个独立的名字，就好像：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;中国&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;浙江省&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;温州市&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;永嘉县&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;乌牛镇&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;河口埭村&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;河口逮村&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;王麻子&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这种封闭的结构充分的避免了名字冲突。而且显得非常的自然。&lt;/p&gt;

&lt;p&gt;但是很可惜的是，es在继承python的import时，却捡了芝麻丢了西瓜，丢掉了这个重要特性。关于这方面，我们在后面的文章继续的探讨。&lt;/p&gt;

&lt;h3 id=&quot;3-从cc-include到ruby-require&quot;&gt;3. 从c/c++ include到ruby require&lt;/h3&gt;

&lt;p&gt;ruby，几乎和python一样，出生在那个计算机语言大爆炸的年代。自然也面临了这种挑战。ruby采用了和python并不完全相同的require去解决相似的历史问题。&lt;/p&gt;

&lt;p&gt;就像是import解决了头文件和源代码文件分离，解决了循环引用和重复引用。require也解决了同样的问题。&lt;/p&gt;

&lt;p&gt;但要说require和import最大的区别的话，那就是require从出生的那天起，其实就是一个函数，而import却是一个指令。换句话来说，你可以这么写代码：&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;my_func&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'some_module'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;do_something&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;但是你不能这么写代码&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;my_func&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
    &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;some_module&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;some_module&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;do_something&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这也是require和include的最大区别。&lt;/p&gt;

&lt;p&gt;这个区别看上去是细微的，但是实际上却造成非常的微妙：&lt;/p&gt;

&lt;p&gt;import可以在代码执行前扫描依赖关系，如果某个依赖不存在的话，解释器有机会在不执行一行代码的前提下就直接报错停止。（虽然实际上python并没有这么做）。相似的，代码打包器也完全可以在不执行代码的前提下为代码打包，语法提示器完全可以在不执行代码的前提下进行语法提示。。。&lt;/p&gt;

&lt;p&gt;听起来非常的美妙，但是同样的，import静态性自然有静态性的缺点：&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;plugin_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'img_'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ext&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plugin_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 非常的OK，因为require本来就是个函数，传个字符串啥的分分钟的&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;plugin_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'img_'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ext&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;plugin_name&lt;/span&gt;     &lt;span class=&quot;c1&quot;&gt;# 囧，加载了一个模块叫plugin_name
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;还有另一个缺陷也无法忽视，就是加载时间，随着现在软件项目越来越大，启动时一次性加载到内存即漫长又没有必要，因此即使是python，现实实现中也是遇到一个import执行一个，并不是一次性先全部检查加载。（但即使是这样，python的启动速度还是非常的慢，核心的原因是import的语法设计会引导使用者总是先把所有依赖都写在文件头部）&lt;/p&gt;

&lt;p&gt;而require则完全是相反的路子，依赖完全是动态加载的。就像是事物的两面，require解决了静态依赖的痛点，但同时动态化也使得ruby的语法提示变得困难。打包器也不得不以来于一个独立的Gemfile来描述依赖管理。&lt;/p&gt;

&lt;p&gt;顺便值得一提的是，ruby require作为一个函数，同时还获得了一些意外的好处，就是require函数是可以被覆写的，因此诞生了一个伟大的项目，叫rubygems。虽然在ruby之前perl已经做了cpan，但是不得不说中央式的包仓库还是被ruby发扬光大的。但这其实也不算是require的优点，因为在import上也完全可以做。&lt;/p&gt;

&lt;h3 id=&quot;4-nodejs的抉择&quot;&gt;4. node.js的抉择&lt;/h3&gt;

&lt;p&gt;动态/静态就像是光暗的两面，也是这次js require/import之争的核心。从某种角度来说，我个人认为python import的设计在总体上是优于ruby require的。其中最大的点就是，ruby require采用了和c/c++类似的设计：当你require一个文件的时候，这个文件的所有名字就会全部暴露在当前的名字空间下。&lt;/p&gt;

&lt;p&gt;当node.js项目开始的时候，js是一个完全完全没有包模型的语言，所有的代码都被运行在一个叫window的变量的全局名字空间下。要在js里加载另一个js完全是一个魔法，我相信熟悉前端的老同学们下面代码应当都不陌生：&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 谜之js加载远程脚本&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;script&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;createElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;onload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;script loaded&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;src&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;https://mywebsite.com/myscript.js&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;head&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;appendChild&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;所以当node.js开始的时候，所面临的第一个问题就是：如果让一个js能加载另一个js？&lt;/p&gt;

&lt;p&gt;问题很复杂，但这个抉择却是半点都不复杂，node.js是2009年由Ryan Dahl单独开始的，这和node的竞争对手go来说，形成了极其鲜明的对比。go语言背后有强大的Google作为支撑，而node几乎是白手起家。这几乎注定了在这方面成本是致命的衡量标准。&lt;/p&gt;

&lt;p&gt;实现一个require并不困难，只需要在V8引擎中注入一个全局变量既可。但是如果要实现一个import的话，却需要修改V8引擎的词法分析、语法分析、几乎牵扯到了解释器的方方面面。从这个角度上来说，node采用require是一个完全不意外的选择。&lt;/p&gt;

&lt;p&gt;但是同样是require，Ryan却从ruby里充分的吸取了历史教训：node require会返回一个变量而不是修改本地名字空间。&lt;/p&gt;

&lt;p&gt;这个小小的改动一方面降低了成本，另一方面解决了两个重大问题：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;node解决了ruby名字冲突问题，拥有了和python类似的名字隔离方案&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;node解决了ruby插件加载后难以接收插件根对象的问题&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;另外node require还做了一个极具前瞻性的事情，就是把依赖安装到了项目目录。这个看似简单的改动，成功的绕开了python/ruby作为后端开发所面临的dependencies hell。相信用python的同学一定知道那个该死的virtualenv，或者ruby的rvm。连go语言这种后发的语言都掉入了GOHOME的坑里。&lt;/p&gt;

&lt;p&gt;node的require不是没有缺点，而是基于实用主义的最佳实践。甚至说node require是现存语言最成功的设计都不为过。&lt;/p&gt;

&lt;h3 id=&quot;5-ecmascript标准化委员会的选择&quot;&gt;5. ECMAScript标准化委员会的选择&lt;/h3&gt;

&lt;p&gt;那么node require设计是否就是完美的设计呢？&lt;/p&gt;

&lt;p&gt;勿需质疑，肯定不是。因为完美只存在于童话世界。软件工程就是妥协和权衡的集中营。完美的设计，今天不存在，今后也不会存在。&lt;/p&gt;

&lt;p&gt;为什么es标准化委员会选择重新造个轮子？&lt;/p&gt;

&lt;p&gt;当然，我们可以推行些阴谋论：因为require的方案是廉价的，import的方案是昂贵的。强势玩家推行一个困难的方案有利于逼迫弱势玩家出局。&lt;/p&gt;

&lt;p&gt;但咱今天不是来聊阴谋论的，咱来聊一聊“官方说法”：require方案不利于网络预加载，加载大量的文件会导致网页加载速度低下。&lt;/p&gt;

&lt;p&gt;就像是我上面说的，动态方案的最大缺陷就在于无法被简单的静态分析。当浏览器加载页面如果需要反复的暂停，等待网络请求，的确会极大的影响用户体验。这个从表明上来说貌似是一个非常“合理”的理由。我们暂且认为这个理由是合理的。但es标准化委员会在import的设计中犯的低级错误简直无法忍受。&lt;/p&gt;

&lt;p&gt;es的import基本上来说就是以python的import为蓝本设计的，反正你就是横着看像，竖着看也像。但偏偏却和python的不同。最大的不同在哪？就是default。&lt;/p&gt;

&lt;p&gt;在python中，一切都是导出的，但在es中，你可以用export关键字选择导出什么。这是一个现代化设计的趋势，并没有任何问题。&lt;/p&gt;

&lt;p&gt;但在es中，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import xxx from 'xxx'&lt;/code&gt;，却并不是导入xxx的所有东西，而是等价于&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import { default as xxx } from xxx&lt;/code&gt;。es import的默认语法并不是导入所有的东西，而是导入了一个叫default的东西并重命名。&lt;/p&gt;

&lt;p&gt;这是一个非常不可思议的设计！对比一下下面的js/python代码：&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// *错误代码*  这两行代码导入了同样的东西&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;天安门&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;北京市&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 相当与  导入`北京市.default`并取名为`天安门`&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;长安街&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;北京市&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 相当与  导入`北京市.default`并取名为`长安街`&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;天安门&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;北京市&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 这是正确导入&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;北京市&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;北京市&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 这句等价于python: import 北京市&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# 这两行代码导入了不同的东西
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;北京市&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;天安门&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 相当与  导入`北京市.天安门`
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;北京市&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;长安街&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 相当与  导入`北京市.长安街`
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在浏览器的环境下，我们希望网络流量越小越好，我们肯定是不希望在一个包里塞一些乱七八糟没用的东西，然后让浏览器下载到用户的电脑的。但这并不是重点，重点是这个奇怪的行为完完全全彻彻底底的破坏了名字的封装性。考虑一下下面的代码：&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;王麻子&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;张家村&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;王麻子&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;李家屯&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;WTF？？？？&lt;/p&gt;

&lt;p&gt;那我怎么办？这么写吗？&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;王麻子&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;张家村的王麻子&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;张家村&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;王麻子&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;李家屯的王麻子&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;李家屯&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;不知道大家怎么觉得，我反正是觉得蛋隐隐约约的疼。&lt;/p&gt;

&lt;p&gt;我相信这个时候es的拥护者会告诉我说，你可以这么写代码啊？&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;张家村&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;张家村&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;李家屯&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;李家屯&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;OK，我们来回顾一下Babel的人是怎么指责require的吧？&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Babel采用一种“摇晃大树”的方法来进行js模块的裁剪。在node require中，用户会把整个模块require进来，我们没法知道一个模块中哪些部分是需要打包的，哪些部分是不需要打包的，只能把整个模块打包进来。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;你这么做和require有什么区别？除了写的更长更折腾之外？&lt;/p&gt;

&lt;p&gt;我们上面举的王麻子的例子某种程度上来说还不够“致命”。更加致命的是：在现代工程方法里，如果一个方法所在的上下文是清晰的，那么它不应该重复清晰了的部分。什么意思呢？就是下面这个意思。&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sha1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 而不是 sha1.sha1_init()&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;sha1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;hello&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 而不是 sha1.sha1_update(...)&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;sha1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;final&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 而不是 sha1.sha1_final()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;有了上面这个例子，我们再来看一下下面的代码&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sha1&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 这中间有3个屏幕的代码&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// balabala&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 我init了啥？&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;hello&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// update？？？哪来的方法？谁的方法？&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;final&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// final what???&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如果这个时候你还有个模块叫aes要导入的话，我觉得你真的是骂娘的心情都有了。因为aes也有3个函数叫init, update, final。&lt;/p&gt;

&lt;p&gt;OK，那怎么写？我能想到的唯一写法只能是这样：&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;init&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sha1_init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
         &lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sha1_update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
         &lt;span class=&quot;nx&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sha1_final&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;sha1&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;init&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aes_init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
         &lt;span class=&quot;nx&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aes_update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
         &lt;span class=&quot;nx&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aes_final&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aes&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;当然，这不是es标准化委员会的唯一失误。&lt;/p&gt;

&lt;p&gt;node.js的开发人员2017年2月在blog说：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;由于import和现行require的水货不容，经过权衡了大量的方案后，我们只能为新的es模块开启了全新的扩展名&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*.mjs&lt;/code&gt;，我们把这个新的文件扩展名热切的称之为麦克杰克逊脚本(Michael Jackson Script)。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;文章过去了1年半了，至今node.js仍然没法完美的融合两套系统。推行一套无法和现行事实标准兼容的新标准，无疑是一个搬石头砸脚的行为。&lt;/p&gt;

&lt;p&gt;然而前面两个问题虽然严重，却不是最严重的，最严重的问题是：&lt;/p&gt;

&lt;p&gt;标准委员会的工作应当是致力于社区的融合，esModule却导致了严重的社区分裂。Babel做法更是进一步的挑起战火。&lt;/p&gt;

&lt;h3 id=&quot;6-import真的完成了es赋予的使命吗&quot;&gt;6. import真的完成了es赋予的使命吗？&lt;/h3&gt;

&lt;p&gt;其实说到这里，我想这到底是进车还是倒车，大家心里都有自己的看法了。但我还想最后谈谈，import真的完成当初的使命了吗？&lt;/p&gt;

&lt;p&gt;明显是没完成。从各方面来说都是彻头彻尾的没完成。&lt;/p&gt;

&lt;p&gt;从标准化进度来说：2015年6月的标准，到现在只有chrome和safari实现了。后端js更是深陷泥潭。&lt;/p&gt;

&lt;p&gt;从出发点（优化加载速度）来说：脚本即使不执行，也需要一个一个从网上下下来才能知道到底哪个脚本依赖了哪个脚本。&lt;/p&gt;

&lt;p&gt;但这些都不重要，重要的是：import并行加载完全就是个伪需求。&lt;/p&gt;

&lt;p&gt;做过前端的都知道一个东西，叫：atlas。atlas是啥？其实就是一个大拼图：由于网络请求零碎图标会导致大量的网络请求，而导致页面加载速度下降。有经验的前端程序员会把大量的小图标打包在一起，放在一张大图上。然后用css的background属性“扣”出图片的一个部分。&lt;/p&gt;

&lt;p&gt;使用atlas能够极大的加快网页的加载速度。其实webpack完全充当了类似的角色：把一堆零碎的js文件打包压缩在一起。&lt;/p&gt;

&lt;p&gt;如果去掉webpack的，让浏览器直接import，完全就是不切实际的做法。一个网页通常只有上百个图标。但一个lodash都有631个文件。如果按照100ms的延迟、10个线程并发、启用tcp fast open、禁用https，在这么极端的情况下仍然需要6.4秒。这仅仅是一个lodash。未经压缩的jQuery有多少文件，未压缩的d3有多少文件？一个页面到时候10分钟加载够吗？&lt;/p&gt;

&lt;p&gt;所以浏览器到底需要什么？需要的只是一个简单的异步require，一个能让前面提到的那个魔鬼的加载script方法滚蛋就好了。&lt;/p&gt;

&lt;h3 id=&quot;7-以后会怎么样&quot;&gt;7. 以后会怎么样？&lt;/h3&gt;

&lt;p&gt;虽然我很想吐嘈import，也一直在吐嘈import。但是esModule已经不可回避的成为了js的标准。就像是阿斗终将接替刘备的位置。&lt;/p&gt;

&lt;p&gt;但是其实事情不会有那么悲观，就像是糟糕的C++标准化委员会催生了Qt。import之后必定会有“民间组织”为import擦屁股。&lt;/p&gt;

&lt;p&gt;但今天我们能怎么办？&lt;/p&gt;

&lt;p&gt;如果你是标准化的忠实信徒，那么尽可能避开import和require结合的雷区：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;不要使用export default，这样能减少和require混用的兼容性问题&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;尽可能的只导出构造函数(或者说类)，这样能绕开很多名字空间问题&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;如果你是想我一样的实用化主义者，那么：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;安心的使用require，不要去碰import&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;用插件去hack webpack，让代码看起来更合理&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;无视webpack“大佬们”的“正确代码指南”&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;前端项目的寿命一般都很短，不需要考虑明天。真有那么一天，sed或者awk都是好东西。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;最后：当一个吃瓜群众，等标准委员会的同志们最后达成一致，或等撑不下去了的玩家退场。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;</content><author><name>Chizhong Jin</name><email>pjincz@gmail.com</email></author><category term="javascript" /><category term="node.js" /><category term="es6" /><category term="zh" /><summary type="html">由于“工作”需要，在上周的某个月黑风高的夜晚，“新建”了一个vue项目。当然了，像我这种不做死就会死星人，自然“一不小心”就把webpack升级到了3.x。 然后惊喜的发现var Vue = require('Vue');竟然不工作了？？？然后再发现var MyComponent = require('./MyComponent');竟然也不工作了？？？ 然后就是一路的Google，WHY??? 回头换来一个说法竟然是为了“全方位”的兼容import/export，从babel xxx版本开始，require必须后面跟个.default才能导入export的结果。 我擦，我这是瞬间要炸毛的节奏啊。以牺牲require用户体验的方式，强制“退休”require。你们真的“赢了”。 本来吧，es标准委员会推出esModule标准的时候，就想吐嘈这完全是个开历史倒车的标准。但是本着“和谐共处 x项原则”。你推你的import，我用我的require，各走阳光道。但是这次的事件真的是着实恶心到我了。 本来想写篇文章喷import，但是想想作为一个老年人，还是火气还是不要这么大，与其互喷，还不如写点文章，来聊聊require是如何走到今天的吧。相信各位看官看完，自然对import是进车还是倒车就有了自己的看法了。 1. 从c/c++ include说起 在“上古时代”，文件系统啥的都不存在，自然也不存在什么include/import/require的了。在那个年代，一切的“函数”都被编译好了，安安静静的躺在寻址空间的某个角落。 当我们需要某个“函数”的时候，我们只需要说：“啊，我知道一个人叫张三，长的人高马大，一米六八。”就可以了。 换成c的代码就是： void printf(char * fmt, ...); 这就是所谓的“声明”。很多刚学c的同学很是难以理解，声明和定义区别在哪里。这个其实蛮正常的啦，就像是生活在现代社会的人，难以很多“传统”习俗都是蛮难理解的。(就像是为什么要喝热水，手工狗脸) 但是人嘛，都是懒得，程序员那是尤其的懒。一遍一遍的写累是一方面，更是容易出错。我相信眼尖的同学已经发现问题了：为什么fmt的类型是char *而不是const char *？因为：我就是故意的啊。。。 眼睛更尖同学应该还会注意到printf的返回值，对的，printf应该返回int，而不是void。来源于那个年代奇怪的信仰。如果不是对着手册抄的话，我估计现在多数人都会弄错。 那这个错的离谱的代码能跑吗？答案是“能”！这个明显的不合理啊，这简直不是自欺欺人么？？？显然，我们的“祖先们”也发现了这个问题。在原始朴素的想法驱动下，那自然就是把一部分重复的东西提取出来，include自然也就出现了。 #include &quot;那些我懒得重复写的声明.h&quot; 显然的，这种朴素的include和现代的包管理有着天壤之别。inlcude赋予程序员灵活性的同时，也带来了一堆苦恼。相信下面代码是个c/c++的程序员都写过： #ifndef __MY_PROJECT_MY_WIDGET_H__ #define __MY_PROJECT_MY_WIDGET_H__ ... #endif//__MY_PROJECT_MY_WIDGET_H__ 在历史上，头文件里是只有函数声明的，在c语言里你对同一个函数声明1次或者声明100次，编译器完全都是“无动于衷”，自然也不会有防重复导入的功能了。甚至不少c/c++的代码还依赖于这种重复导入的功能。 再一个典型的例子就是include的顺序问题，相信写过c++的人，对include hell啥的都是不陌生的。 include只是一个萌芽，问题和坑几乎漫山遍野，程序猿从来都是解决一个问题时创造两个问题生物。做包管理肯定也不例外。include虽然带来了漫山遍野的问题，但include解决了当年的核心诉求。我们在这里就不细数include的问题了，不公正也没必要。 2. 从c/c++ include到python import 从c/c++ include到python或者java的import可以说是编程历史上的一个重大进步。个人没有花时间去追溯到底是python借鉴了java，还是java借鉴了python，也或许它们共同的受到了另一个语言的启发。在这里之所以以python为例，是因为es的import是以python import为原型设计的。 但不论如何，import出现解决了大量的问题。 例如你不需要在写一个独立的头文件，因为本质上来说，头文件完全是可以通过源文件生成的。 再例如就是你不再需要担心文件之间的互相引用关系，也不需要再担心文件的引用顺序，文件的防重复引用。 甚至python也许有意也许无意下解决了一个c/c++的痛点，就是名字空间问题：假如村子里有两个叫王麻子的怎么办？ 在c/c++年代，大家普遍的使用前缀来解决这个问题：既然村子里有两个王麻子，这个事情好办，一个取名成村头的王麻子，另一个取名为村尾的王麻子问题就解决了。 当然了，如果你要把一个镇子的人都拉下来一起写代码，王麻子们自然就不能这么取名字了，自然就出现了里岙村村头王麻子、外山村村尾王麻子。 这个看起来貌似没什么的问题，当随着越来越多的程序员参与开发后，问题越来越爆炸。最后发展到什么样子呢？我们来看一个gtk的函数命名： void gtk_menu_tool_button_set_arrow_tooltip_markup(...); 这个不是我特意挑特意选的，而是我随手打开gtk的手册挑了一个，这个函数命名足足有45个字符。这显然写起来不是那么舒服。 但即使是这样，名字冲突还是层出不穷。最典型的就是c/c++里面那些让人崩溃的“基本拓展类型”，比如啥DWORD啊，UINT32啊，到处都在定义，还都不太一致。一出错完全不知道引了哪个定义。 我为什么说我不知道python到底是有意还是无意解决了这个问题呢？ 在python的import语法中，当你导入一个包时，包里面的东西是不会展开到你的名字空间的。而是会有一个独立的名字，就好像： import 中国.浙江省.温州市.永嘉县.乌牛镇.河口埭村 print(河口逮村.王麻子) 这种封闭的结构充分的避免了名字冲突。而且显得非常的自然。 但是很可惜的是，es在继承python的import时，却捡了芝麻丢了西瓜，丢掉了这个重要特性。关于这方面，我们在后面的文章继续的探讨。 3. 从c/c++ include到ruby require ruby，几乎和python一样，出生在那个计算机语言大爆炸的年代。自然也面临了这种挑战。ruby采用了和python并不完全相同的require去解决相似的历史问题。 就像是import解决了头文件和源代码文件分离，解决了循环引用和重复引用。require也解决了同样的问题。 但要说require和import最大的区别的话，那就是require从出生的那天起，其实就是一个函数，而import却是一个指令。换句话来说，你可以这么写代码： def my_func require('some_module') do_something end 但是你不能这么写代码 def my_func(): import some_module some_module.do_something() 这也是require和include的最大区别。 这个区别看上去是细微的，但是实际上却造成非常的微妙： import可以在代码执行前扫描依赖关系，如果某个依赖不存在的话，解释器有机会在不执行一行代码的前提下就直接报错停止。（虽然实际上python并没有这么做）。相似的，代码打包器也完全可以在不执行代码的前提下为代码打包，语法提示器完全可以在不执行代码的前提下进行语法提示。。。 听起来非常的美妙，但是同样的，import静态性自然有静态性的缺点： plugin_name = 'img_' + ext require(plugin_name) # 非常的OK，因为require本来就是个函数，传个字符串啥的分分钟的 plugin_name = 'img_' + ext import plugin_name # 囧，加载了一个模块叫plugin_name 还有另一个缺陷也无法忽视，就是加载时间，随着现在软件项目越来越大，启动时一次性加载到内存即漫长又没有必要，因此即使是python，现实实现中也是遇到一个import执行一个，并不是一次性先全部检查加载。（但即使是这样，python的启动速度还是非常的慢，核心的原因是import的语法设计会引导使用者总是先把所有依赖都写在文件头部） 而require则完全是相反的路子，依赖完全是动态加载的。就像是事物的两面，require解决了静态依赖的痛点，但同时动态化也使得ruby的语法提示变得困难。打包器也不得不以来于一个独立的Gemfile来描述依赖管理。 顺便值得一提的是，ruby require作为一个函数，同时还获得了一些意外的好处，就是require函数是可以被覆写的，因此诞生了一个伟大的项目，叫rubygems。虽然在ruby之前perl已经做了cpan，但是不得不说中央式的包仓库还是被ruby发扬光大的。但这其实也不算是require的优点，因为在import上也完全可以做。 4. node.js的抉择 动态/静态就像是光暗的两面，也是这次js require/import之争的核心。从某种角度来说，我个人认为python import的设计在总体上是优于ruby require的。其中最大的点就是，ruby require采用了和c/c++类似的设计：当你require一个文件的时候，这个文件的所有名字就会全部暴露在当前的名字空间下。 当node.js项目开始的时候，js是一个完全完全没有包模型的语言，所有的代码都被运行在一个叫window的变量的全局名字空间下。要在js里加载另一个js完全是一个魔法，我相信熟悉前端的老同学们下面代码应当都不陌生： // 谜之js加载远程脚本 var script = document.createElement('script'); script.onload = function() { console.log('script loaded'); } script.src = 'https://mywebsite.com/myscript.js'; document.head.appendChild(script); 所以当node.js开始的时候，所面临的第一个问题就是：如果让一个js能加载另一个js？ 问题很复杂，但这个抉择却是半点都不复杂，node.js是2009年由Ryan Dahl单独开始的，这和node的竞争对手go来说，形成了极其鲜明的对比。go语言背后有强大的Google作为支撑，而node几乎是白手起家。这几乎注定了在这方面成本是致命的衡量标准。 实现一个require并不困难，只需要在V8引擎中注入一个全局变量既可。但是如果要实现一个import的话，却需要修改V8引擎的词法分析、语法分析、几乎牵扯到了解释器的方方面面。从这个角度上来说，node采用require是一个完全不意外的选择。 但是同样是require，Ryan却从ruby里充分的吸取了历史教训：node require会返回一个变量而不是修改本地名字空间。 这个小小的改动一方面降低了成本，另一方面解决了两个重大问题： node解决了ruby名字冲突问题，拥有了和python类似的名字隔离方案 node解决了ruby插件加载后难以接收插件根对象的问题 另外node require还做了一个极具前瞻性的事情，就是把依赖安装到了项目目录。这个看似简单的改动，成功的绕开了python/ruby作为后端开发所面临的dependencies hell。相信用python的同学一定知道那个该死的virtualenv，或者ruby的rvm。连go语言这种后发的语言都掉入了GOHOME的坑里。 node的require不是没有缺点，而是基于实用主义的最佳实践。甚至说node require是现存语言最成功的设计都不为过。 5. ECMAScript标准化委员会的选择 那么node require设计是否就是完美的设计呢？ 勿需质疑，肯定不是。因为完美只存在于童话世界。软件工程就是妥协和权衡的集中营。完美的设计，今天不存在，今后也不会存在。 为什么es标准化委员会选择重新造个轮子？ 当然，我们可以推行些阴谋论：因为require的方案是廉价的，import的方案是昂贵的。强势玩家推行一个困难的方案有利于逼迫弱势玩家出局。 但咱今天不是来聊阴谋论的，咱来聊一聊“官方说法”：require方案不利于网络预加载，加载大量的文件会导致网页加载速度低下。 就像是我上面说的，动态方案的最大缺陷就在于无法被简单的静态分析。当浏览器加载页面如果需要反复的暂停，等待网络请求，的确会极大的影响用户体验。这个从表明上来说貌似是一个非常“合理”的理由。我们暂且认为这个理由是合理的。但es标准化委员会在import的设计中犯的低级错误简直无法忍受。 es的import基本上来说就是以python的import为蓝本设计的，反正你就是横着看像，竖着看也像。但偏偏却和python的不同。最大的不同在哪？就是default。 在python中，一切都是导出的，但在es中，你可以用export关键字选择导出什么。这是一个现代化设计的趋势，并没有任何问题。 但在es中，import xxx from 'xxx'，却并不是导入xxx的所有东西，而是等价于import { default as xxx } from xxx。es import的默认语法并不是导入所有的东西，而是导入了一个叫default的东西并重命名。 这是一个非常不可思议的设计！对比一下下面的js/python代码： // *错误代码* 这两行代码导入了同样的东西 import 天安门 from '北京市' // 相当与 导入`北京市.default`并取名为`天安门` import 长安街 from '北京市' // 相当与 导入`北京市.default`并取名为`长安街` import { 天安门 } from '北京市' // 这是正确导入 import * as 北京市 from '北京市' // 这句等价于python: import 北京市 # 这两行代码导入了不同的东西 from 北京市 import 天安门 # 相当与 导入`北京市.天安门` from 北京市 import 长安街 # 相当与 导入`北京市.长安街` 在浏览器的环境下，我们希望网络流量越小越好，我们肯定是不希望在一个包里塞一些乱七八糟没用的东西，然后让浏览器下载到用户的电脑的。但这并不是重点，重点是这个奇怪的行为完完全全彻彻底底的破坏了名字的封装性。考虑一下下面的代码： import { 王麻子 } from '张家村' import { 王麻子 } from '李家屯' WTF？？？？ 那我怎么办？这么写吗？ import { 王麻子 as 张家村的王麻子 } from '张家村' import { 王麻子 as 李家屯的王麻子 } from '李家屯' 不知道大家怎么觉得，我反正是觉得蛋隐隐约约的疼。 我相信这个时候es的拥护者会告诉我说，你可以这么写代码啊？ import * as 张家村 from '张家村' import * as 李家屯 from '李家屯' OK，我们来回顾一下Babel的人是怎么指责require的吧？ Babel采用一种“摇晃大树”的方法来进行js模块的裁剪。在node require中，用户会把整个模块require进来，我们没法知道一个模块中哪些部分是需要打包的，哪些部分是不需要打包的，只能把整个模块打包进来。 你这么做和require有什么区别？除了写的更长更折腾之外？ 我们上面举的王麻子的例子某种程度上来说还不够“致命”。更加致命的是：在现代工程方法里，如果一个方法所在的上下文是清晰的，那么它不应该重复清晰了的部分。什么意思呢？就是下面这个意思。 var c = sha1.init(); // 而不是 sha1.sha1_init() sha1.update(c, 'hello'); // 而不是 sha1.sha1_update(...) sha1.final(c); // 而不是 sha1.sha1_final() 有了上面这个例子，我们再来看一下下面的代码 import { init, update, final } from sha1 // 这中间有3个屏幕的代码 // balabala var c = init(); // 我init了啥？ update(c, 'hello'); // update？？？哪来的方法？谁的方法？ final(c); // final what??? 如果这个时候你还有个模块叫aes要导入的话，我觉得你真的是骂娘的心情都有了。因为aes也有3个函数叫init, update, final。 OK，那怎么写？我能想到的唯一写法只能是这样： import { init as sha1_init, update as sha1_update, final as sha1_final } from sha1 import { init as aes_init, update as aes_update, final as aes_final } from aes // ... 当然，这不是es标准化委员会的唯一失误。 node.js的开发人员2017年2月在blog说： 由于import和现行require的水货不容，经过权衡了大量的方案后，我们只能为新的es模块开启了全新的扩展名*.mjs，我们把这个新的文件扩展名热切的称之为麦克杰克逊脚本(Michael Jackson Script)。 文章过去了1年半了，至今node.js仍然没法完美的融合两套系统。推行一套无法和现行事实标准兼容的新标准，无疑是一个搬石头砸脚的行为。 然而前面两个问题虽然严重，却不是最严重的，最严重的问题是： 标准委员会的工作应当是致力于社区的融合，esModule却导致了严重的社区分裂。Babel做法更是进一步的挑起战火。 6. import真的完成了es赋予的使命吗？ 其实说到这里，我想这到底是进车还是倒车，大家心里都有自己的看法了。但我还想最后谈谈，import真的完成当初的使命了吗？ 明显是没完成。从各方面来说都是彻头彻尾的没完成。 从标准化进度来说：2015年6月的标准，到现在只有chrome和safari实现了。后端js更是深陷泥潭。 从出发点（优化加载速度）来说：脚本即使不执行，也需要一个一个从网上下下来才能知道到底哪个脚本依赖了哪个脚本。 但这些都不重要，重要的是：import并行加载完全就是个伪需求。 做过前端的都知道一个东西，叫：atlas。atlas是啥？其实就是一个大拼图：由于网络请求零碎图标会导致大量的网络请求，而导致页面加载速度下降。有经验的前端程序员会把大量的小图标打包在一起，放在一张大图上。然后用css的background属性“扣”出图片的一个部分。 使用atlas能够极大的加快网页的加载速度。其实webpack完全充当了类似的角色：把一堆零碎的js文件打包压缩在一起。 如果去掉webpack的，让浏览器直接import，完全就是不切实际的做法。一个网页通常只有上百个图标。但一个lodash都有631个文件。如果按照100ms的延迟、10个线程并发、启用tcp fast open、禁用https，在这么极端的情况下仍然需要6.4秒。这仅仅是一个lodash。未经压缩的jQuery有多少文件，未压缩的d3有多少文件？一个页面到时候10分钟加载够吗？ 所以浏览器到底需要什么？需要的只是一个简单的异步require，一个能让前面提到的那个魔鬼的加载script方法滚蛋就好了。 7. 以后会怎么样？ 虽然我很想吐嘈import，也一直在吐嘈import。但是esModule已经不可回避的成为了js的标准。就像是阿斗终将接替刘备的位置。 但是其实事情不会有那么悲观，就像是糟糕的C++标准化委员会催生了Qt。import之后必定会有“民间组织”为import擦屁股。 但今天我们能怎么办？ 如果你是标准化的忠实信徒，那么尽可能避开import和require结合的雷区： 不要使用export default，这样能减少和require混用的兼容性问题 尽可能的只导出构造函数(或者说类)，这样能绕开很多名字空间问题 如果你是想我一样的实用化主义者，那么： 安心的使用require，不要去碰import 用插件去hack webpack，让代码看起来更合理 无视webpack“大佬们”的“正确代码指南” 前端项目的寿命一般都很短，不需要考虑明天。真有那么一天，sed或者awk都是好东西。 最后：当一个吃瓜群众，等标准委员会的同志们最后达成一致，或等撑不下去了的玩家退场。</summary></entry><entry><title type="html">游戏本，也许真的不太适合用来办公</title><link href="https://mechanical-consciousness.com/2018/09/21/game-book-is-not-good-for-work-zh.html" rel="alternate" type="text/html" title="游戏本，也许真的不太适合用来办公" /><published>2018-09-21T00:00:00+08:00</published><updated>2018-09-21T00:00:00+08:00</updated><id>https://mechanical-consciousness.com/2018/09/21/game-book-is-not-good-for-work-zh</id><content type="html" xml:base="https://mechanical-consciousness.com/2018/09/21/game-book-is-not-good-for-work-zh.html">&lt;p&gt;前段时间接手了一个3d项目。琢磨琢磨手头的Thinkpad x230的确不太适合做3d开发，另外也年代就已，给自己找了个合适借口，入了个技嘉aero 15x。&lt;/p&gt;

&lt;p&gt;刚拿到aero 15x的时候，的确着实惊艳。纤细的机身，卓越的性能。靓丽细腻的屏幕。每个细节都让人觉得爱不释手。&lt;/p&gt;

&lt;p&gt;但是经过了一个多月的使用后，饱受煎熬，最近只能又迁回了x230。游戏本毕竟是游戏本，开发办公什么的，还是真的不适当。坐在飞机上也无聊，顺便把一个多月的使用体验写下来。也算的上是深度评测了吧？就算是给打算买游戏本来办公的朋友们一点点参考吧。&lt;/p&gt;

&lt;h3 id=&quot;1-糟糕的输入体验&quot;&gt;1. 糟糕的输入体验&lt;/h3&gt;

&lt;p&gt;不得不说，aero 15x的键盘设计真的别具匠心，也很炫酷十足。举个例子来说，大部分游戏本都有一个没法解决的毛病，就是发热。尤其是打大型3d游戏的时候，n卡绝对是一个火炉子。aero 15x非常用心的在asdw下面放置了一个小的入风口。吃起鸡来，不但不会烤爪子，还感觉凉嗖嗖的。&lt;/p&gt;

&lt;p&gt;BUT，这个到了办公的时候，完全就不是个需求。相反各种毛病就开始暴露了：僵硬的按钮，糟糕的键盘防抖和防误触，经常按不出来的中括号，不标准的键盘键位。。。每一个毛病都是一个死穴。&lt;/p&gt;

&lt;p&gt;我本以为这只是习惯的问题，但是逞强了一个月后，在手腕和指节的疼痛下，只能承认，键盘和键盘之间是存在“差异”的。&lt;/p&gt;

&lt;p&gt;还有一个就是触摸板，用惯了thinkpad的小红点，用触摸板真的很难受，但我觉得这个点可能是个人的毛病吧。但是aero 15x的触摸板真的太大了，挤压了原本属于键盘的空间。导致整个键盘变得更加难以使用。另外为了避免误触触摸板，右手长期都是悬空的，这进一步加重了右手的疲劳。&lt;/p&gt;

&lt;h3 id=&quot;2-无时无刻的发热和噪音&quot;&gt;2. 无时无刻的发热和噪音&lt;/h3&gt;

&lt;p&gt;aero 15x带了一块7700的u和一块1060的卡，这个配置就算是在PC上也算是不错的了。伴随而来的，自然是发热。&lt;/p&gt;

&lt;p&gt;刚拿到本子的时候是夏天，默认的风扇设置下，无论什么时候，键盘都是热呼呼的。这当然不是我想要的。一顿折腾后，把系统风扇设置为总是慢速工作。刚开始，这个真的效果不错。噪音和温度都很不错。但是1个月后，我充分的认识到了，我做了一个相当错误的选择。持续一个月的低速工作后，风扇开始出噪音了。只要一开机，就能听到持续不断的咔咔声。我们可以说这是质量问题，但是风扇毕竟是机械件，只要是机械件必定有磨损。长期的磨损下来，有点噪音啥的太正常了。迫不得已，只能重新关掉风扇，继续享受温热的键盘。&lt;/p&gt;

&lt;h3 id=&quot;3-基本无法使用的电池模式&quot;&gt;3. 基本无法使用的电池模式&lt;/h3&gt;

&lt;p&gt;伴随这发热的另一个问题，就是用电。aero 15x官方宣称可以待机4个小时。第一次实际测试虽然没有那么理想，但是感觉还是能凑合的。&lt;/p&gt;

&lt;p&gt;但是，后面使用的时候发现，这个本子一拔电源，基本上就完全卡的不行了。做什么都是卡卡顿顿的。打开一下maya，5-10分钟起不来。换句话说，续航是上来了，但是你也别想干活了。节能模式下，你只就只能看看小说了。至于说关闭节能？我是没试过，估计也不是能用的。&lt;/p&gt;

&lt;h3 id=&quot;4-没法在飞机上和床头使用&quot;&gt;4. 没法在飞机上和床头使用&lt;/h3&gt;

&lt;p&gt;飞机上（也包括火车和汽车啦），打开aero 15的时候，基本上那个小板子是空间不够用的。如果没有小板子的时候，那抱着更是不现实。对于我这种在飞机上闲不住的人来说，这个真心是一个大缺点啊。（当然这也说明了我这是穷啊，买不起商务仓，哈哈哈）&lt;/p&gt;

&lt;p&gt;然后自然的，没法在飞机上用，也没法在床头上用。真是一大遗憾啊。&lt;/p&gt;

&lt;h3 id=&quot;结论&quot;&gt;结论&lt;/h3&gt;

&lt;p&gt;经过了一个月的折腾，最后还是换回了老thinkpad。用回老thinkpad的刹那。手指有种起飞的感觉。&lt;/p&gt;

&lt;p&gt;总的感觉来说吧，游戏本就是游戏本，从设计以来就是为了解决游戏的痛点。办公啥的还是不要自虐了，弄个小商务本去吧。&lt;/p&gt;</content><author><name>Chizhong Jin</name><email>pjincz@gmail.com</email></author><category term="zh" /><summary type="html">前段时间接手了一个3d项目。琢磨琢磨手头的Thinkpad x230的确不太适合做3d开发，另外也年代就已，给自己找了个合适借口，入了个技嘉aero 15x。 刚拿到aero 15x的时候，的确着实惊艳。纤细的机身，卓越的性能。靓丽细腻的屏幕。每个细节都让人觉得爱不释手。 但是经过了一个多月的使用后，饱受煎熬，最近只能又迁回了x230。游戏本毕竟是游戏本，开发办公什么的，还是真的不适当。坐在飞机上也无聊，顺便把一个多月的使用体验写下来。也算的上是深度评测了吧？就算是给打算买游戏本来办公的朋友们一点点参考吧。 1. 糟糕的输入体验 不得不说，aero 15x的键盘设计真的别具匠心，也很炫酷十足。举个例子来说，大部分游戏本都有一个没法解决的毛病，就是发热。尤其是打大型3d游戏的时候，n卡绝对是一个火炉子。aero 15x非常用心的在asdw下面放置了一个小的入风口。吃起鸡来，不但不会烤爪子，还感觉凉嗖嗖的。 BUT，这个到了办公的时候，完全就不是个需求。相反各种毛病就开始暴露了：僵硬的按钮，糟糕的键盘防抖和防误触，经常按不出来的中括号，不标准的键盘键位。。。每一个毛病都是一个死穴。 我本以为这只是习惯的问题，但是逞强了一个月后，在手腕和指节的疼痛下，只能承认，键盘和键盘之间是存在“差异”的。 还有一个就是触摸板，用惯了thinkpad的小红点，用触摸板真的很难受，但我觉得这个点可能是个人的毛病吧。但是aero 15x的触摸板真的太大了，挤压了原本属于键盘的空间。导致整个键盘变得更加难以使用。另外为了避免误触触摸板，右手长期都是悬空的，这进一步加重了右手的疲劳。 2. 无时无刻的发热和噪音 aero 15x带了一块7700的u和一块1060的卡，这个配置就算是在PC上也算是不错的了。伴随而来的，自然是发热。 刚拿到本子的时候是夏天，默认的风扇设置下，无论什么时候，键盘都是热呼呼的。这当然不是我想要的。一顿折腾后，把系统风扇设置为总是慢速工作。刚开始，这个真的效果不错。噪音和温度都很不错。但是1个月后，我充分的认识到了，我做了一个相当错误的选择。持续一个月的低速工作后，风扇开始出噪音了。只要一开机，就能听到持续不断的咔咔声。我们可以说这是质量问题，但是风扇毕竟是机械件，只要是机械件必定有磨损。长期的磨损下来，有点噪音啥的太正常了。迫不得已，只能重新关掉风扇，继续享受温热的键盘。 3. 基本无法使用的电池模式 伴随这发热的另一个问题，就是用电。aero 15x官方宣称可以待机4个小时。第一次实际测试虽然没有那么理想，但是感觉还是能凑合的。 但是，后面使用的时候发现，这个本子一拔电源，基本上就完全卡的不行了。做什么都是卡卡顿顿的。打开一下maya，5-10分钟起不来。换句话说，续航是上来了，但是你也别想干活了。节能模式下，你只就只能看看小说了。至于说关闭节能？我是没试过，估计也不是能用的。 4. 没法在飞机上和床头使用 飞机上（也包括火车和汽车啦），打开aero 15的时候，基本上那个小板子是空间不够用的。如果没有小板子的时候，那抱着更是不现实。对于我这种在飞机上闲不住的人来说，这个真心是一个大缺点啊。（当然这也说明了我这是穷啊，买不起商务仓，哈哈哈） 然后自然的，没法在飞机上用，也没法在床头上用。真是一大遗憾啊。 结论 经过了一个月的折腾，最后还是换回了老thinkpad。用回老thinkpad的刹那。手指有种起飞的感觉。 总的感觉来说吧，游戏本就是游戏本，从设计以来就是为了解决游戏的痛点。办公啥的还是不要自虐了，弄个小商务本去吧。</summary></entry></feed>