破坏性更新——亡羊补牢
Destructive Updates – A Stitch in Time

原始链接: https://icicle-lang.github.io/posts/2025-02-01-a-time-travelling-optimisation.html

Icicle 是一种高级流式查询语言,编译为 C 代码以提高速度。为了保持纯度,数组变异最初需要复制,这导致性能瓶颈。这篇文章详细介绍了一种编译时优化方法,可以消除不必要的数组复制,从而将运行时间减少高达 50%。 解决方案使用自定义的“拼接图”(Stitching Graph)来跟踪数组引用及其在名为 Avalanche 的低级 DSL 中的用法。通过使用“时间旅行单子”(Tardis Monad)对程序的抽象语法树 (AST) 执行前向和后向遍历,编译器可以识别在潜在复制后不再使用的数组。如果在变异之后没有其他引用可能指向该数组,则可以消除复制操作,从而允许破坏性更新。这种方法的灵感来自于 R 等语言中的引用计数,避免了运行时开销。“拼接图”快速达到不动点(fixpoint)的能力是效率的关键,这对于处理 Icicle 查询中常见的嵌套循环至关重要。该算法展示了图分析和单子编程相结合如何在纯函数式语言中实现显著的性能改进。

Hacker News 最新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 破坏性更新——及时缝补 (icicle-lang.github.io) g0xA52A2A 2小时前 4 分 | 隐藏 | 过去 | 收藏 | 讨论 加入我们,参加 6 月 16-17 日在旧金山举办的 AI 初创公司学校! 指导原则 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系我们 搜索:

原文

Posted on February 1, 2025 by Huw Campbell


How the Tardis Monad and a Stitching Graph helps discover affine array usage, permitting destructive updates.

Icicle is a high-level streaming query language, which gives new capabilities to its users, allowing them to combine and fuse hundreds of rich, individual, queries into a combined plan for safe and efficient execution.

At the heart of the language is our Core intermediate language, which we’ve spoken about optimising in an earlier post. This is a pure, simply-typed lambda calculus based DSL with restrictions on arbitrary recursion and closure creation.

To ensure speed, we compile all queries to C and convert almost all of our data types into simple, unboxed C types. For example, the Either Int Bool type will compile into three C variables on the stack, indicating the tag, and variables for the left and right values.

Maps and arrays in the language, however, compile using the struct of arrays approach when transitioning to C; compiling down to potentially many C arrays of simple types. However, as Icicle Core is a pure language, during lowering the compiler inserts copy operations to arrays before performing any mutations upon them.

This is the story of how we eliminate the overwhelming majority of these copy operations from idiomatic Icicle code by performing destructive updates during operations such as inserting into a map or sorting an array, while maintaining query semantics and reducing run-times by up to 50%.

Motivating Queries

It’s very common in Icicle queries to use the built in group context, which acts somewhat like an SQL group by. For example, the following query looks at a stream of injuries, then computes a map of locations to the sum of the injuries’ severities.

Advanced R).

Other languages which employ this technique include the Lean proof assistant and functional programming language and Koka, which reuses constructors (like list’s cons) too.

Icicle – Compilation Time Copy Elision

In Icicle, we avoid using reference counting and instead use a simple bump allocator per entity. We do this because:

  • Except for arrays and strings all bindings are placed directly onto the stack;
  • It’s fast;
  • Each entity runs separately and is bounded by the number of events; and
  • It makes clearing memory when we’ve finished processing an entity close to trivial.

That said, we should still aim to reduce new array allocations though for speed and memory efficiency, but here we do it entirely at compile time.

The constraints for this optimization are:

  • It should remove copies if the original array will not be accessed again.
  • It must not alter the results of a query.
  • It must not unduly slow down compilation.

To eliminate a copy operation, we need to identify two key factors:

  • All references that might point to the array at the time of copying.
  • All references that are used after the copy is made.

If there are no subsequent usages of any reference that might point to the array about to be copied, we can eliminate the copy operation and simply alias the binding instead.

But this presents a challenge: all the references we need to consider arise from before the copy operation; while all the usages come from after it. So at any point in the query, we need to have performed both forward and backward passes over the AST to know what to do.

Finally, as Icicle’s semantics are essentially a strict left fold, which is compiled to a for loop, our internal DSL must contain looping structures (for and while). One can imagine that even the definition of future and past accesses could get a little muddy, as each loop could reference data from previous iterations.

Avalanche - Our Internal DSL

We perform copy elision within a low level internal DSL called avalanche. This is a small imperative language which contains basic if and foreach statements; and can create, read from, and update mutable variables. It does not perform IO.

As a Haskell type, the language looks a bit like this:

tardis package, which supplies the Tardis monad.

In the above code listing, we swap to the Tardis monad and replace get with getPast and modify with modifyForwards. Also, every time we see a Name in either a Var in Exp or as the named item in, for example, a Read, we send backwards the usages (free variables)

Counting Immutable Beans is an open question.

Application in a Strict Language

The discovery of this algorithm was challenging, it took time and iteration – and even though I could intuitively see copy operations which could be removed, writing an even close to O(n) algorithm was proving to be trifficult.

It was only when I remembered Phil Freeman’s Tardis Monad water problem solution that I managed to solidify my ideas and really nail down the algorithm.

Laziness, purity, and monadic composition were critical in allowing me to discover this algorithm.

That said, I think one could make this work in a strict language, by adopting a relatively straight forward, explicit two pass approach, where in the forwards pass, instead of returning a Statement value, we return a function, Usage -> (Statement, Usage); and pass the usage set backwards while building the graph in our reverse pass.

联系我们 contact @ memedata.com