• Install Ruby on Apple Silicon at 2020年11月28日

    之前说 Ruby 3 JIT 性能不好的一个主要问题是 x64 上 L1i 命中很低。感觉上来说 M1 同时加大了 L1i 的大小和分页大小,可能 JIT 的提升会更大一点,但实际上好像带来的提升很小,甚至没有。一种可能是因为 RISC 生成的指令数量也比较多两者抵消了,还有一种可能就是这点大小增加效果不明显。

    但反过来来看这个堆 ALU 单元,堆发射数量带来的单核 IPC 提升堆解释型语言的提升还是比较明显的。

    但最好还是跑个 Sinatra 的测试,因为 Sinatra L1i 命中问题比 optcarrot JIT 严重很多,需要研究看看这个问题在 M1 上是表现得差不多还是变得更严重了。

  • ffi 的那个问题已经有个 PR 了,原先没有 arm64-darwin 的 type 描述,实在不行可以上那个 fork 过的版本。

    msgpack 这个怪怪的,因为 msgpack 有给瘦 gem,按道理 bundle 应该是本机打出来的,不知道怎么会缺 arch。感觉是苹果编译的解释器有点问题。

  • 我没有 M1 的机器。

    但是 ARM Linux 上编译 ruby 是没有问题的,M1 上的 macOS 的 LLVM 应该也是没有问题的。而且 macOS 还有自带的 ruby 解释器,四舍五入一下我倾向于认为没有什么问题。

  • 我对 .NET 5 最期待的是 Unity 什么时候能有意愿从 Mono 上迁移到 CoreCLR 了,可以有效改善一下那个烂 GC 在很多游戏中造成的奇怪卡顿。然后 Web 开发可能得看看 Blazor 框架的发展情况。

  • 会有这个现象,而且实际情况非常复杂。通常情况下每个核心有自己的 L1 L2 缓存,同一个 CCX/NUMA 节点会共享 L3 缓存。一个线程被操作系统从一个核调度到另外一个核心执行的时候,可能更容易遇到缓存失效的问题。但操作系统会优先调度到同一个 NUMA 节点上,同时当这种计算密集场景出现的时候,线程的 nice 值会被降低,系统会让线程多执行一会再去执行别的东西。手动绑定会不会得到改善,其实是有疑问的。

  • Ruby 3 Fiber 变化前瞻 at 2020年11月17日

    mysql2 的 C 实现提供阻塞和非阻塞两种模式,后者可以在 Ruby 上进一步接入 Fiber。但通常 Ruby 上用的是前者。而我们可以用 Fiber 和非阻塞模式封装出一个 I/O 性能更好的,但是使用方法和前者一样的 API。要看 wrapper 的实现社区具体想怎么跟进了。

  • Ruby 3 Fiber 变化前瞻 at 2020年11月16日

    Promise / async / await 根本就是 Fiber Scheduler 语义的等价写法。Fiber Scheduler 要做的就是自动 consume 和 transfer。只有在脱离 Fiber Scheduler 裸写 Fiber 的 Ruby 2.x 里才需要手动 consume 和 transfer。

    Promise 在 JavaScript 里只是为了解决的 callback hell 的替代,如果要用 Promise 只需要加个类就行。这步甚至和调度没有任何关系:

    class Promise
      def initialize(&callback)
        @callback = callback
      end
    
      def then(&resolve)
        @callback.call(resolve)
      end
    end
    

    而如果要有全局的 async await 关键字支持,在有 fiber scheduler 的情况下,transfer 也已经自动完成了,只要把 fiber chain 起来就行了:

    ##
    # Meta-programming Kernel for Syntactic Sugars
    module Kernel
      # Make fiber as async chain
      # @param [Fiber] fiber root of async chain
      def async_fiber(fiber)
        chain = proc do |result|
          next unless result.is_a? Promise
          result.then do |val|
            chain.call(fiber.resume(val))
          end
        end
        chain.call(fiber.resume)
      end
    
      # Define an async method
      # @param [Symbol] method method name
      # @yield async method
      # @example
      #   async :hello do 
      #     puts 'Hello'
      #   end
      def async(method)
        define_singleton_method method do |*args|
          async_fiber(Fiber.new {yield(*args)})
        end
      end
    
      # Block the I/O to wait for async method response
      # @param [Promise] promise promise method
      # @example
      #   result = await SQL.query('SELECT * FROM hello')
      def await(promise)
        result = Fiber.yield promise
        if result.is_a? PromiseException
          raise result.payload
        end
        result
      end
    end
    

    这点上根本不需要 Ruby MRI 解释器做额外的支持。

  • 没懂,现在的 MRI VM 确实是 Ruby -> ByteCode -> C Runtime 这样的解释过程。这和 JVM/JavaScript 的 VM 架构不是差不多吗?相比之下 JavaScript JIT 介入的位置还更早,感觉更不纯 VM 一点才对吧(?)

  • 尝试使用 Ruby 3 调度器 at 2020年10月18日

    Goroutine 的 newm 和 new 是不一样的。newm 是启用系统的 Thread,而 new 是 (newproc) 对于 Fiber (Continuation) 的封装。new (newproc) 会在一些情况下触发 newm。这就是「 Fiber 的内部协作式调度,再和整体的 Thread 一起做出来的封装」而不单纯是系统 Thread。

    随便看个 https://golang.org/src/runtime/asm_amd64.s 252 行,就是内部 gosave,和 Fiber 实现完全一样。如果直接调用系统 Thread 自然就不用这东西了。

  • 尝试使用 Ruby 3 调度器 at 2020年09月27日

    你把 parallelism 并行性和 concurrency 并发性的概念搞混了。这是两个完全不同的概念。GIL 能不能 Parallel,和能不能做到 Concurrent 是两个完全不同的概念。

    如果一个应用是 I/O boundary,靠多核来解决问题是非常不恰当的。因为多核依赖操作系统的 Threading 线程调度,比在程序内进行上下文切换反而是更慢的。Windows 3.0、Mac OS 9 后操作系统摒弃协作式调度的本质是为了优化使用上的体验,单从性能角度出发实际上是在变得更低的。

    至于你说 “还不如学 go 封装一下 thread”,事实上 Goroutine 的 Thread 并不是真正的 Thread 的封装并不能因为看起来暴露了一个类似 Thread 的接口,就认为这是 Thread。只要你熟悉一下 Goroutine 的实现就会发现,其也是如 Fiber 的内部协作式调度,再和整体的 Thread 一起做出来的封装,是多线程多协程切换的实现。

    协作式调度之所以快的原因也很简单,如果你检查 Linux 线程实现的汇编的话你就会知道抢占调度有多复杂,不但需要一个 syscall 本身的开销,还需要计算前一个线程使用的 cpu time,还需要处理其提前返回的原因,维护 fair 值的红黑树,设置 CPU 中断,然后才能切换。而协作调度单纯只要找到下一个可用的协程,然后切换几个 CPU 上的寄存器即可。与其说是和线程抢 cpu time,不如说从操作系统的复杂调度机制中解放了更多的 cpu time。

    不止是 Goroutine,任何高效的 Web 服务器实现,比如 nginx 之类都有内置的上下文切换来提高 I/O 效率,其和 Fiber 是完全一样的原理。然后 nginx 将其再和多线程的模型进行结合,这也是可以在之后引入 Guild 后操作的。调度非阻塞连接是非常复杂的,如果单纯一个 nonblocking 关键字就能做到 nonblocking 的话,又何来不同的操作系统 API 的异步性能差异呢?如果你觉得加入 nonblocking 后只要遍历所有的请求,不但会导致 cpu 性能的极大浪费,最终也只能实现和图中 IO.select 差不多的性能。当然,如果你用 C 语言简单写一个基于 select 的 nonblock 服务器,也可以轻松跑个 10k qps,这是因为其内存占用非常小和简单,操作系统切换代价更小,从而让你觉得调度本身不会影响 qps 的错觉。而如果你正确在 C 语言上实现一个 epoll 的最简单的代码,那可就是十万甚至百万级别的并发了。这也是为什么我们在做性能分析的时候,要控制变量。使用不科学的条件设计出来的实验,其结果必然也是不科学的。

    在 Linux 最新版本上,甚至进一步引入一个 io_uring 来进一步优化这一问题。因为这不光会涉及对 nonblocking 请求监听的性能,甚至连操作系统内部对 I/O 缓冲区的处理,甚至用户态内存拷贝的效率都会影响这个过程的性能。

    至于一个 I/O 往 另一个 I/O write 1GB 的数据,既不涉及并发也不涉及并行,当然是阻塞操作的开销更小,因为非阻塞操作引入了额外的 overhead。但实际上的 workload 都是同一个程序内多个 I/O 的调度。只有涉及了 “调度”,我们才需要 “调度器”,而单纯基于操作系统线程的调度太慢了,这就是我们要自己进一步实现的原因。

    Ruby 3 Fiber Scheduler 确实是一个魔术。基于手写也是可以实现类似的效果的。而且需要特别注意的是,如果我们的直接场景是 Web 服务器,我们完全可以认为 API 之间的状态是隔离的,状态是由数据库来维护的。所以我们可以在 Fiber 外面再套一层 fork 就可以进一步利用上多核。这么做的有比如 socketry/falcon,qps 可以轻松上十万。puma 的多进程模型已经不受 GIL 影响了,只要内存够,想把 cpu time 跑满也是很轻轻松松的事情,那么为什么 falcon 又会比多核多进程模型的 puma 更是快上了十几倍呢?因为 Fiber 调度比操作系统调度 Thread 更节省 cpu time。同样的 cpu time,Fiber 调度做的有用工作比较多,而 puma 只是在浪费它占用的那么多 cpu time 而已。

  • WeakMap 是围绕 GC 的设计,Object#freeze 是动态类型语言对 final 关键字的补偿设计。 这个要追溯的话,应该还是要追溯到 Lisp。无论是 Ruby 还是 JavaScript 都是深受 Lisp 启发的语言。 而且 Lisp + OOP 的话第一反应就是 CLOS 了。确实 Lisp 衍生语言的特性大同小异的。

  • 夸张。。。夸张手法。。。

  • rbtree 的 patch 当时一个设计是用来优化 SortedSet 的 hash 实现,但是现在 Ruby 的 hash 在 4 年前就改由 Open Addressing 的方法来实现了([Feature #12142])。这就类似于 Java Spark 里面的 OpenHashMap 了,性能远优于闭散列的实现,自然相关的东西就否决了。

    如果是实际的算法用途,我记得很早以前 GSoC 有个 Ruby 的算法相关 gem 的实现。但这东西能不能进标准库,我表示怀疑。不过目前线下的算法比赛,不管是 ACM/ICPC 还是 OI 应该都是不能用 Ruby 的吧。如果是 Codeforces 或者 Leetcode,虽然不能用 gem,但 gem 也就是 require 的 ruby 文件,提前展开一下做成模板就是了。

  • Ruby 3 Fiber 变化前瞻 at 2020年07月27日

    谢谢,已修复 typo。

  • gems 安装或添加源时卡住 at 2020年05月09日

    如果是 IPv6 连接的话,应该会涉及到

    DNS4 DNS6
    IPv4 DNS4 + IPv4 DNS6 + IPv4
    IPv6 DNS4 + IPv6 DNS6 + IPv6

    这么一个 2x2 的问题。可以先用 https://ipv6-test.com/ 这网站确认自己 IPv6 是正常工作的。看解析出来的 2409:8c54::/32 确实是中国移动机房的地址,像是一个合理的 CDN 节点,不过我这里 IPv6 肯定是好的的情况下打过去也是 100% 丢包。

  • 可以可以,我安排一下。

  • std::bad_alloc 是 C++ 内存申请失败的异常吧。申请失败的常见原因应该是... 你机器上内存已经被吃爆掉了,分配不出可用内存了?

  • 乐理还是讲相对关系,标准音高其实是可以变的,调律方式也是可以变的。所以我只是用了其中一种比较常用的来写了,毕竟写代码最好还是确定算法确定数值比较好处理一点。

  • Sonic Pi 我也用过,算是可以很方便用 Ruby 来为合成器进行编程。特别是现在高级的合成器按钮越来越多,真的还不如直接弄个 DSL 来写比较方便。

  • 其实本来是想在线下的分会场弄一个小的 workshop 尝试的。但现在搬到线上后,互动性没有那么强了,是有点直播 coding 的味道了。

  • 差不多比起 Keynote 演讲的纯介绍性,更多地加入 Live Coding 来演示某一种技术的使用。以练习和 tutorial 为主,内容不一定需要太先进或困难。

  • Petri Net workflow for Rails at 2020年02月14日

    促进一下生态,我又花了一天写了个 PetriNet 的可视化编辑器 https://github.com/dsh0416/petri-editor

  • 花了两天时间简单写了个 DSL https://github.com/dsh0416/petri-dsl/

    用法:

    require 'petri'
    
    network = Petri::Net.new do |net|
        net.start_place :start, name: 'Start'
        net.end_place :end, name: 'End'
    
        net.transition :leader_evaluate, name: 'Leader Evaluate', consume: :start do |t|
            t.produce :leader_approved, name: 'Leader Approved', with_guard: :approved
            t.produce :rejected, name: 'Rejected', with_guard: :rejected
        end
    
        net.transition :hr_evaluate, name: 'HR Evaluate', consume: :leader_approved do |t|
            t.produce :hr_approved, name: 'HR Approved', with_guard: :approved
            t.produce :rejected, with_guard: :rejected
        end
    
        net.transition :report_back, name: 'Report Back', consume: :hr_approved, produce: :end
    
        net.transition :resend_request, name: 'Resend Request', consume: :rejected do |t|
            t.produce :start, with_guard: :resend
            t.produce :end, with_guard: :discard
        end
    end
    
    puts network.compile
    
    # {:places=>[{:label=>:start, :name=>"Start"}, {:label=>:end, :name=>"End"}, {:label=>:leader_approved, :name=>"Leader Approved"}, {:label=>:rejected, :name=>"Rejected"}, {:label=>:hr_approved, :name=>"HR Approved"}], :transitions=>[{:label=>:leader_evaluate, :name=>"Leader Evaluate", :consume=>[:start], :produce=>[{:label=>:leader_approved, :guard=>:approved}, {:label=>:rejected, :guard=>:rejected}]}, {:label=>:hr_evaluate, :name=>"HR Evaluate", :consume=>[:leader_approved], :produce=>[{:label=>:hr_approved, :guard=>:approved}, {:label=>:rejected, :guard=>:rejected}]}, {:label=>:report_back, :name=>"Report Back", :consume=>[:hr_approved], :produce=>[{:label=>:end, :guard=>nil}]}, {:label=>:resend_request, :name=>"Resend Request", :consume=>[:rejected], :produce=>[{:label=>:start, :guard=>:resend}, {:label=>:end, :guard=>:discard}]}], :start_place=>:start, :end_place=>:end}
    

    大概可以相对方便地来描述 workflow 了。

  • 我觉得维护一个 OpenCV 的 wrapper 可能先得维护一个线性代数库,返回一个 Array,甚至是 Array 的 Array 的 Array 还是有点蠢的,而且很难处理。ruby 现在有内建的 Matrix 库,不知道能不能堪此大任。

  • 我比较好奇「帮包装」是个什么流程