MIT PDOS Noria Database Delivers 14M Ops/Sec Using Lock-Free Rust

Rust at speed — building a fast concurrent database — Jon Gjengset

Jon Gjengset

Eliminating reader-writer locks in favor of epoch-tracked dual maps allows write-driven dataflow databases to double the throughput of dedicated in-memory caches.

AI-assisted summary of Rust at speed — building a fast concurrent database — original source. Check the original for full context.

Source published:

Key ideas

  • Noria, a 60,000-line research database prototype built in Rust at MIT CSAIL, replaces read-time query execution with write-driven incremental dataflow, turning reads into O(1) materialized view lookups. (1:21)
  • Standard reader-writer locks (RwLock) fail under fast, short critical sections like hash map lookups because pthreads internally serializes reads using an exclusive lock to manage reader counts. (27:57)
  • Using a dual-buffered Left-Right map with epoch tracking (evmap), Noria achieves 14 million operations per second on vote-count queries, outperforming memcached (7M ops/sec) and relational engines like MySQL (200k ops/sec). (40:19)
  • The core lock-free evmap data structure relies on only 7 lines of unsafe Rust code, demonstrating that high-performance concurrent systems can isolate manual invariants to minimal, auditable blocks. (37:27)

Detailed notes

## Background Published on YouTube by Jon Gjengset on January 5, 2019, this video records a guest lecture delivered at Two Sigma Labs in November 2018. According to the page description, the talk covers Gjengset's experience using Rust to build large, high-performance systems, focusing on the architecture of Noria—a research database prototype built at MIT—and a specialized concurrent data structure designed for it.

## TL;DR Gjengset explains how traditional relational databases waste CPU cycles recomputing queries for read-heavy workloads and demonstrates how Noria addresses this by maintaining dynamic, self-updating materialized views through write-driven incremental dataflow. To eliminate contention on the shared results cache without stalling reads, he breaks down the Left-Right concurrency pattern and reader-epoch mechanics behind the lock-free `evmap` data structure. Microbenchmarks and realistic Lobsters production traces demonstrate Noria substantially outperforming MySQL, commercial databases, and Memcached under high concurrency. He concludes by offering practical advice for adopting Rust, illustrating how to work collaboratively with the borrow checker, leverage tooling like Clippy, and embrace compiler-guided refactoring.

## Atmosphere & style The presentation carries the engaging, articulate cadence of an advanced systems seminar, seamlessly bridging high-level language philosophy with low-level mechanical sympathy. Following Larry Rudolph's warm, collegiate introduction, Gjengset delivers an approachable yet deeply technical walkthrough, punctuating complex concurrency concepts with dry humor and reassuring calm around taboo topics like `unsafe`. It feels like an intimate masterclass from a battle-tested systems engineer who genuinely empathizes with the friction of the borrow checker while revealing how its strict discipline makes programmers fundamentally better at reasoning about correctness.

## Bulletin - [01:21](https://www.youtube.com/watch?v=s19G6n0UjsM&t=81s) Noria, a 60,000-line research database prototype built in Rust at MIT CSAIL, replaces read-time query execution with write-driven incremental dataflow, turning reads into O(1) materialized view lookups. - [27:57](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1677s) Standard reader-writer locks (RwLock) fail under fast, short critical sections like hash map lookups because pthreads internally serializes reads using an exclusive lock to manage reader counts. - [40:19](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2419s) Using a dual-buffered Left-Right map with epoch tracking (evmap), Noria achieves 14 million operations per second on vote-count queries, outperforming memcached (7M ops/sec) and relational engines like MySQL (200k ops/sec). - [37:27](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2247s) The core lock-free evmap data structure relies on only 7 lines of unsafe Rust code, demonstrating that high-performance concurrent systems can isolate manual invariants to minimal, auditable blocks.

## Key takeaways - [03:19](https://www.youtube.com/watch?v=s19G6n0UjsM&t=199s) Rust breaks traditional systems programming trade-offs by combining C-level performance and zero-cost abstractions with compile-time memory safety and data-race prevention. - [07:11](https://www.youtube.com/watch?v=s19G6n0UjsM&t=431s) Algebraic data types like Result and Option, paired with expressive pattern matching, eliminate null-pointer dereferences and force exhaustive compile-time error handling. - [09:20](https://www.youtube.com/watch?v=s19G6n0UjsM&t=560s) Integrated tooling such as Cargo, automatic executable doctests, and frictionless C/C++ FFI make Rust development ergonomic and practical for incremental adoption. - [10:58](https://www.youtube.com/watch?v=s19G6n0UjsM&t=658s) Rust remains challenging for rapid prototyping, GUI programming, bare-metal no_std kernels, and vendor-heavy C frameworks like Intel DPDK due to strict compiler guardrails and manual binding overhead. - [13:35](https://www.youtube.com/watch?v=s19G6n0UjsM&t=815s) Web architectures are heavily read-dominated, but traditional relational databases repeatedly recompute expensive joins and aggregates, while external caches introduce invalidation bugs and thundering herd failures. - [16:09](https://www.youtube.com/watch?v=s19G6n0UjsM&t=969s) Noria addresses database read bottlenecks by maintaining precomputed state incrementally on writes through a directed dataflow graph of relational operators. - [18:37](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1117s) Handling real-world workloads in Noria required solving partial materialization to prevent unbounded memory growth and enabling live, dynamic dataflow graph migrations without tearing down running state. - [21:01](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1261s) Rust's ownership and borrow checker enforce safety by ensuring memory is either accessed via multiple immutable references (&T) or a single exclusive mutable reference (&mut T) at compile time. - [27:57](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1677s) For short operations like hash map lookups, reader-writer locks bottleneck throughput because acquiring and releasing the lock requires internal serialization that dwarfs the lookup time. - [30:27](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1827s) Rust's unsafe keyword does not disable all compiler guarantees; it specifically permits low-level operations like dereferencing raw pointers while localizing verification to auditable blocks. - [33:15](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1995s) The Left-Right pattern avoids read locks by maintaining duplicate maps, swapping atomic read/write pointers, and tracking reader epochs to detect when concurrent readers have cleared old state. - [40:19](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2419s) On an end-to-end trace replay of the Lobsters web service, Noria handled 5,000 to 6,000 page views per second on a 16-core machine compared to 800 to 1,100 on MySQL. - [43:23](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2603s) Learning Rust requires understanding ownership semantics rather than treating it like C or Go; compiler friction typically signals unidiomatic designs such as cyclic pointer graphs. - [46:57](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2817s) Decomposing systems into modular crates improves architecture, speeds up parallel compilation, and allows enforcing documentation coverage with compiler flags like #![deny(missing_docs)]. - [49:32](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2972s) The rigor of the Rust type system enables fearless compiler-guided refactoring, turning compiler error diagnostics into an actionable checklist when altering complex systems. - [51:02](https://www.youtube.com/watch?v=s19G6n0UjsM&t=3062s) Internalizing Rust's borrow checker alters how developers reason about concurrency and resource ownership across all other programming languages.

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=s19G6n0UjsM&t=0s) Introduction and Jon Gjengset's Background

Larry Rudolph introduces the guest lecture hosted by the Two Sigma Labs team, explaining that one of the team's core missions is bridging industry with academia. Rudolph recalls meeting Jon Gjengset at MIT CSAIL while working with Trevor on firmware security:

> “We had this idea that we should use Rust and so we met, so there's the MIT Rust expert, or the Northeast Rust expert, or the worldwide Rust expert, I don't know which one call yourself with, but his research is also on databases, novel ways databases, and coordination and conflicts and stuff that I really like.”

Rudolph outlines Gjengset's geographic trajectory—growing up in Norway, completing his undergraduate degree in Australia, moving to London, and ultimately coming to MIT as a graduate student in CSAIL. Rudolph asked Gjengset to cover both topics: the Rust programming language and database research. Gjengset confirms that the lecture will be followed by a deep dive and extended Q&A session.

[01:21](https://www.youtube.com/watch?v=s19G6n0UjsM&t=81s) Talk Roadmap: Rust in Practice and the Noria Database

Jon Gjengset sets expectations for the lecture, stating that the talk addresses Rust from a practical perspective based on his three years of building Noria at MIT. Noria is a high-performance research database prototype developed within the Parallel and Distributed Operating Systems (PDOS) group. Gjengset explains that he wants to demystify what it feels like to build a real, large-scale production-grade system in Rust, directly addressing common industry reservations about the language.

```mermaid flowchart TD accTitle: Scope of the Talk accDescr: High-level overview of covered and excluded topics in Jon Gjengset's talk. subgraph Covered["Topics Covered"] direction TB C1["High-level Rust & ownership fundamentals"] C2["Noria high-performance database architecture"] C3["Novel concurrent data structure design"] C4["Practical experience building large systems"] end subgraph Excluded["Deferred to Q&A / Research Paper"] direction TB E1["Syntax deep-dives & low-level compiler code"] E2["Query planning & query execution internals"] E3["Formal database theory"] end ```

Rather than focusing on syntax or low-level compiler details, the talk concentrates on higher-level trade-offs, Rust's ownership model, and the overall architecture of Noria. Gjengset also highlights that he will present a specialized concurrent data structure designed specifically for Noria that should appeal to systems engineers. Query execution and planning details are left to the published paper or follow-up discussions.

- Noria comprises approximately 60,000 lines of Rust code developed over three years. - Jon Gjengset maintains multiple open-source Rust libraries, contributes to Rust's asynchronous I/O ecosystem, and has published over 30 hours of live Rust coding streams.

[03:19](https://www.youtube.com/watch?v=s19G6n0UjsM&t=199s) What Is Rust? Fast, Reliable, Productive

Gjengset explains that Rust is an open-source systems programming language created by Mozilla that reached version 1.0 roughly four years prior. While systems programming is traditionally a vague term applied to servers, clients, and infrastructure software, Gjengset emphasizes that Rust aims to fundamentally eliminate long-standing compromises in systems software design.

> “One of the ones they're relatively well-known for is this 'fast, reliable, productive, pick three', and the idea is that in a lot of existing production languages you're sort of forced into this 'I have to choose away something, I have to choose to not get a particular feature'—like in C you don't get safe code, but you get really high performance, and Rust wants to sort of do away with those trade-offs instead saying let's try to do all of them.”

A primary example is Rust's philosophy of "fearless concurrency," enabling engineers to author highly concurrent systems without constant fear of subtle data races or runtime memory corruption. Gjengset also emphasizes Rust’s open, community-driven governance: development takes place publicly on GitHub, language and compiler enhancements go through an open Request for Comments (RFC) process, and stable releases ship on a predictable six-week cadence.

[05:01](https://www.youtube.com/watch?v=s19G6n0UjsM&t=301s) Core Technical Differentiators of Rust

Contrasting Rust with alternative languages like Haskell, OCaml, and Go, Gjengset breaks down the core technical attributes that make Rust uniquely suited for robust systems programming:

- **Expressive Type System:** Much closer to Haskell than to C, allowing complex invariant checks and state constraints to be enforced entirely at compile time. - **Zero-Cost Abstractions:** Interfaces and polymorphic abstraction layers are monomorphized at compile time, matching C and C++ runtime speeds without virtual call penalties where avoidable. - **The Borrow Checker:** Static analysis guarantees memory safety and freedom from data races at compile time before any binary is produced. - **Modern Infrastructure:** The language tooling incorporates decades of lessons learned in package management, compilation, documentation, and continuous integration.

Because Rust is engineered to interoperate seamlessly with existing C and C++ codebases, organizations can selectively swap out fragile, race-prone subcomponents inside legacy software without rewriting entire architectures. Gjengset notes that this interoperability was Mozilla’s original motivation for creating Rust: they needed a way to incrementally replace components of the Firefox browser with safe, concurrent modules that guaranteed absence of crashes. At the same time, the language ergonomically scales up to high-level application services and command-line tooling, feeling almost as expressive as Python while retaining native execution speed and strict compile-time verification.

[07:11](https://www.youtube.com/watch?v=s19G6n0UjsM&t=431s) Memory Safety, Algebraic Data Types, and Pattern Matching

Gjengset highlights concrete features that eliminate broad categories of bugs common in languages like C, C++, and Go. In traditional systems code, developers take it for granted that programs will crash during early runs due to memory errors or concurrency races.

> “In Rust, lots of concurrency bugs are impossible—the compiler will not let you compile a program that has a data race in it, and if you come from basically any other language this is very surprising, right? In C you will write a program and you expect it to crash at runtime the first few times you run it; this is not the case with Rust code.”

Static compile-time ownership semantics completely eliminate use-after-free and double-free vulnerabilities. Beyond memory safety, Gjengset details how Rust structures data and control flow through Algebraic Data Types (ADTs):

| Type / Feature | Rust Implementation | Traditional Language Parallel | Practical Safety Advantage | |:--- |:--- |:--- |:--- | | **Error Handling** | `Result<T, E>` enum with `Ok(T)` and `Err(E)` | Go multi-value returns `(val, err)` or C error codes | The compiler forces explicit checking of both variants before values can be accessed; unhandled errors cannot slip past unnoticed. | | **Absence of Value** | `Option<T>` enum with `Some(T)` and `None` | Null pointers (`NULL`, `nil`, `None`) | Completely removes null-pointer dereference exceptions; values must be unpacked safely. | | **Control Flow** | Rich Pattern Matching | Switch statements | Enables deep destructuring and complex condition matching (e.g., matching a success result containing an integer between 4 and 17 alongside a specific string) with zero runtime cost. |

[09:20](https://www.youtube.com/watch?v=s19G6n0UjsM&t=560s) Rust Tooling, Built-in Documentation, and FFI Interoperability

Rust’s development ergonomics feel remarkably lightweight and modern, despite compiling down to bare-metal performance. Gjengset observes that writing Rust often feels more like Python or JavaScript than low-level C code, largely due to its polished developer tooling. Dependency management is baked directly into the default tooling via Cargo, surpassing package managers like npm for JavaScript, PyPI for Python, or `go get` for Go. It natively handles standard central repository crates, local path dependencies, and remote Git dependencies out of the box without requiring external plugins.

Documentation is similarly treated as a first-class citizen embedded into the language itself. By prefixing any function, module, or constant with three slashes (`///`) instead of two, developers produce markdown documentation. Crucially, code snippets written inside these doc comments are automatically compiled and run as tests during `cargo test`. This guarantees that code examples stay permanently up to date with implementation changes. Furthermore, the central package repository automatically builds, formats, and cross-links all crate documentation, giving the entire ecosystem a uniform, navigable interface. Interoperability with foreign code is equally frictionless: foreign function interfaces (FFI) make calling into C or C++ libraries straightforward, and bidirectional bindings allow seamless integration with languages like Python, Ruby, and Go.

> “It will always be interact with other crates is actually fabulous to use and I do not use that term lightly.” — Jon Gjengset

- **Integrated Package Management:** First-class support for Git repositories, file system paths, and central package registries directly in Cargo. - **Doctests:** Triple-slash doc comments (`///`) execute code samples during unit testing to eliminate stale documentation. - **Foreign Function Interface (FFI):** Direct, low-overhead interop allows existing C, C++, Python, Ruby, or Go applications to incrementally adopt Rust.

---

[10:58](https://www.youtube.com/watch?v=s19G6n0UjsM&t=658s) Current Limitations: Where Rust Struggles

Despite its advantages, Rust is not a silver bullet, and Gjengset emphasizes several domains where the language is still maturing or poorly suited. First, interfacing with highly specialized, vendor-heavy C frameworks—such as Intel’s DPDK (Data Plane Development Kit)—presents high friction. While tools exist to generate bindings from C header files, maintaining vast wrappers for complex APIs requires enormous overhead if nobody in the open-source community has already built them. Second, Rust is fundamentally ill-suited for quick, throwaway prototyping where a developer just wants to write dirty code and run it once without caring about panics. The compiler's relentless insistence on exhaustively handling error states, checking nullable cases via `Option`, and verifying memory safety strictly fights against rough, rapid hacking.

Other problem spaces face ongoing ecosystem growth. Kernel-level development remains finicky because bare-metal code must forgo standard runtime facilities like `libc` and the standard library (`std`). Desktop GUI programming is notoriously challenging across all languages; although Rust has bindings for GTK, Qt, Win32, and macOS Cocoa, the abstractions are young and awkward to map to Rust's ownership model. Finally, asynchronous networking and I/O remain an area of intense work. While Rust's promise-like `Future` abstraction delivers exceptional throughput in high-performance engines like Noria, the ergonomics can become convoluted because higher-level language primitives are still evolving.

> “If you just want to write something and not worry about the details, Rust is an awful language for that job because Rust will force you to write correct code.” — Jon Gjengset

- **Specialized Native Stacks:** Integrating massive C ecosystems like Intel DPDK or complex cloud SDKs incurs heavy binding maintenance. - **Prototype Friction:** Exhaustive type checking, lifetime tracking, and mandatory error handling impede fast, disposable scripting. - **Kernel & GUI Development:** Writing `no_std` kernel extensions requires extra effort, and GUI libraries struggle to align cleanly with Rust's single-ownership model. - **Asynchronous I/O:** The primitives deliver raw performance, but writing complex async dataflows can become visually complex and verbose.

---

[13:35](https://www.youtube.com/watch?v=s19G6n0UjsM&t=815s) The Relational Database Bottleneck and the Cache Problem

Transitioning into the database portion of the lecture, Gjengset introduces Noria, a research database prototype designed to eliminate unnecessary query overhead. In modern web architectures, applications constantly issue SQL queries that join and aggregate relational tables—such as calculating the aggregate vote count for an article by joining a `stories` table with a `votes` table. Traditional relational databases parse the query, devise an execution plan, run the table scans, compute aggregates, join the tables, filter the results, and stream the answer back to the frontend.

```mermaid flowchart TD accTitle: Traditional Database Read Flow vs External Cache accDescr: Diagram contrasting standard database read planning with external cache invalidation challenges.

subgraph RelationalDB ["Traditional Relational Database"] Q["Read Query: SELECT COUNT..."] --> QP["Query Planner"] QP --> Agg["Count Votes"] Agg --> Join["Join with Stories"] Join --> Res["Stream Result"] end

subgraph ExternalCache ["External Cache Pattern"] App["Web Application"] -->|"Check Cache"| Cache["Memcached / Redis"] Cache -.->|"Cache Miss"| DB["Backend Database"] App -->|"Write"| DB App -.->|"Manual Invalidation"| Cache end ```

The fundamental design flaw is that web workloads are overwhelmingly read-heavy, often exceeding 90% read traffic. Because relational engines execute queries upon read, they repeatedly recompute identical joins and aggregates even when underlying data has not changed, wasting immense CPU cycles. Developers typically attempt to patch this inefficiency by deploying an external caching layer like Memcached or Redis. However, external caches burden application code with manual cache invalidation logic. Invalidate too aggressively, and the cache evaporates; invalidate incorrectly, and users serve stale data. Crucially, external caching introduces catastrophic failure modes like the "thundering herd" problem: when a write invalidates an entry for a heavily trafficked key, subsequent concurrent reads all miss simultaneously, overwhelming the database backend at once.

> “All of this seems like so much complexity in order to make your application fast.” — Jon Gjengset

- **Read Dominated Workloads:** Standard applications waste significant CPU power recalculating identical relational views on repetitive reads. - **Invalidation Complexity:** Application developers are forced to manually coordinate cache evictions and writes, opening doors to stale data. - **Thundering Herd Problem:** Evicting a single highly popular key causes concurrent incoming reads to bypass the cache and crush the primary relational database.

---

[16:09](https://www.youtube.com/watch?v=s19G6n0UjsM&t=969s) Noria’s Core Architecture: Incremental Dataflow on Writes

Noria inverts traditional database architecture by observing that the database already knows the queries the application cares about, meaning the database engine itself should maintain the cache. Rather than evaluating a query execution plan on read, Noria evaluates the query incrementally on write. When an application registers a SQL query, Noria constructs an internal dataflow graph consisting of relational operators (such as aggregators, filters, and joins) terminating in a materialized view, which acts as a concurrent in-memory cache.

```mermaid flowchart LR accTitle: Noria Incremental Dataflow Architecture accDescr: Flow diagram tracing an incoming vote write through Noria operators directly updating an in-memory materialized view.

subgraph DataflowPipeline ["Dataflow Pipeline (Executes on Write)"] W["Incoming Write (Vote +1 for A)"] --> CountOp["Count Operator (Updates 41 to 42)"] CountOp -->|"Record: (A, 42)"| JoinOp["Join Operator (Looks up Story Title)"] JoinOp -->|"Record: (A, 42, 'foo')"| MV["Materialized View / Cache"] end

subgraph ApplicationRead ["Application Reads"] Client["Frontend Client"] -->|"Single Direct Lookup"| MV end ```

When a write arrives—such as a new vote for story `A`—it enters the dataflow graph at the base operators. The `count` operator inspects its local state, sees that the prior count was 41, increments it to 42, and emits a delta record containing `(A, 42)`. That record passes downstream into the `join` operator, which resolves story `A`'s title (`"foo"`) and emits `(A, 42, "foo")`. Finally, this record enters the materialized view cache at the leaf node, updating story `A`'s precomputed row directly in memory. When the frontend issues a query, it executes a single instantaneous key-value lookup against this pre-materialized view. The application never performs cache invalidations, writes execute asynchronously through the graph, and the cache remains perpetually updated. Gjengset notes, however, that while this model delivers exceptional throughput, it introduces fundamental research challenges: an engine cannot practically afford to store materialized state for every query and key across all time.

> “The database has your queries, so why can't the database just maintain the cache? Wouldn't that be a lot easier?” — Jon Gjengset

- **Write-Driven Incremental Evaluation:** Queries compile into persistent dataflow graphs that compute state transitions only when base tables change. - **Zero-Invalidation Frontend:** Applications treat the database as a self-updating cache, executing simple O(1) index lookups on read without managing external TTLs. - **Memory Footprint Challenge:** Precomputing and caching every intermediate relation and final view for infinite keys rapidly exhausts physical memory, creating the central engineering challenge Noria must solve.

[18:37](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1117s) Core Challenges in Noria: Partial Materialization and Dynamic Query Adaptation

Jon Gjengset steps back from the basic dataflow architecture to highlight the severe practical challenges encountered when building a real-world database like Noria. The first hurdle is memory exhaustion: systems do not have infinite RAM, meaning data must be evicted from the cache. However, eviction in an incremental dataflow graph is notoriously hard. If the system throws away cached intermediate counts—such as all the computed results for a given "story A"—and an upvote later arrives for that same story, the downstream count operator no longer has the previous vote count available to emit the updated total. As Gjengset puts it: “and so you need a mechanism your system to ensure that even if you throw something away or something gets asked for that you've never been asked for before that you have some way of computing it or recomputing it this is the problem of partial materialization that we will not get into in this talk”.

The second challenge involves handling schema and query evolution over time. While pedagogical examples assume a single, static query, real production applications run hundreds of queries that continuously change, grow, and evolve as features are added. Traditional distributed dataflow runtimes such as Naiad or Apache Spark cannot seamlessly alter query graphs while live traffic is flowing; operators typically have to tear down the entire pipeline and instantiate a brand-new instance containing the updated queries. Noria avoids this brute-force approach by dynamically adapting the dataflow topology on the fly while retaining state—a feat detailed extensively in the Noria research paper.

```mermaid flowchart TD accTitle: Noria Research Challenges accDescr: The three major systems challenges in building Noria.

subgraph Memory ["1. Memory Limits"] A["Cache Eviction"] --> B["Partial Materialization<br>(Recompute on demand)"] end

subgraph Dynamic ["2. Dynamic Topology"] C["Query & Schema Changes"] --> D["Live Graph Rewriting<br>(No tear-down like Spark/Naiad)"] end

subgraph Concurrency ["3. Shared Results State"] E["High-Throughput Reads & Writes"] --> F["Concurrent Results Table<br>(Requires custom synchronization)"] end ```

- **Partial Materialization**: Enables the dataflow engine to compute and store only subset slices of downstream state on demand rather than the entire universe of records. - **Dynamic Migration**: Modifies the operator graph online in response to application migrations without downtime, distinct from batch or static streaming systems. - **Paper Reference**: Gjengset notes that the detailed algorithmic solutions to partial materialization and dynamic dataflow adaptivity can be explored in the published Noria paper.

---

[20:08](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1208s) The Concurrency Bottleneck: Readers vs. Writers in the Results Table

Gjengset focuses on the third critical challenge: synchronizing concurrent access to the materialized results table. In Noria's architecture, this results table serves as the primary cache where all write operations from the upstream dataflow terminate, and simultaneously acts as the destination where client read queries arrive. The table is hammered by hordes of concurrent readers alongside concurrent writes streaming from database updates.

Allowing raw, uncoordinated reads and writes across this shared memory table invites race conditions, torn reads, and data corruption. “You can't just do that unless you have some kind of synchronization because a writer might update a record while it's being written so we need some kind of synchronization here and we want that to be really fast because they're gonna be lots of reads and lots of writes to this at the same time,” Gjengset explains. Finding a synchronization mechanism that does not destroy read throughput while accommodating high write volumes necessitates diving into fundamental computer science principles and examining how Rust models memory access.

---

[21:01](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1261s) Rust's Ownership and Borrowing Model

To understand why traditional synchronization primitives fail this workload, Gjengset breaks down Rust's core ownership mechanism, comparing its underlying philosophy to RAII (Resource Acquisition Is Initialization) in C++. In Rust, every value has a unique owner responsible for reclaiming and freeing its memory when it goes out of scope. Crucially, that owner controls precisely how and when other parts of the program may access that resource.

When handling any arbitrary type `T` (whether an integer, an array, or a string), Rust strictly limits access to one of three variants:

1. **Owned `T`**: You hold exclusive ownership and bear the duty of deallocating it. 2. **Mutable Reference `&mut T`**: Modeled like an exclusive pointer. You gain the privilege of modifying the resource without the duty of freeing it, guaranteed by the compiler that no other thread or pointer can read or write to it concurrently. 3. **Immutable Reference `&T`**: A shared pointer granting read-only access, permitting any number of concurrent readers while strictly forbidding any mutations.

Rust enforces these access contracts at compile time. It checks that a resource never has an active mutable reference (`&mut T`) simultaneously with any immutable references (`&T`), nor may multiple mutable references coexist. Gjengset summarizes the consequence: “it turns out that's all you need to guarantee the program has no data races right if you never have something modify a thing while it's being read or modified you cannot have data races because you will either just have multiple readers or you'll have a single writer which is fine”.

```mermaid flowchart LR accTitle: Rust Borrowing Invariants accDescr: The two mutually exclusive compile-time borrowing states allowed in Rust.

subgraph Shared ["Shared Mode (Safe)"] R1["&T (Reader 1)"] R2["&T (Reader 2)"] RN["&T (Reader N)"] end

subgraph Exclusive ["Exclusive Mode (Safe)"] W["&mut T (Sole Writer)"] end

Shared -. "XOR (Compile Check)".- Exclusive ```

Lifetimes tie borrowed references to the dynamic scope of the owner—such as a specific stack frame. If a function attempts to capture a reference to a stack-allocated buffer and dispatch it to an independent thread that might outlive the current stack frame, the compiler rejects the program during compile time.

- **Double-Free Prevention**: Enforcing a single owner guarantees resources are freed exactly once when that owner drops out of scope. - **Use-After-Free Elimination**: The borrow checker prevents reading from or writing to invalid references by guaranteeing lifetimes never outlive the underlying resource. - **Data-Race Freedom**: The mutual exclusion rule between `&mut T` and `&T` eradicates data races without runtime garbage collection overhead.

---

[24:34](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1474s) Framing Shared Tables as an Ownership Problem: Mutex vs. RwLock

The high-performance results table problem maps directly into Rust's ownership domain: Noria requires concurrent readers and writers to interact with a shared structure simultaneously, which Rust’s compiler outright rejects under raw references. To bridge this gap, Rust requires synchronization types capable of providing safe "interior mutability," allowing a program to turn a shared handle (`&Self`) into an exclusive reference (`&mut T`).

The most canonical synchronization primitive is the `Mutex<T>`. In Rust, a mutex is not an isolated flag sitting next to arbitrary memory; it directly wraps the inner data type `T`. To inspect or update `T`, a caller must invoke `.lock()`, which borrows the mutex immutably and yields an RAII guard (`MutexGuard<'a, T>`). As long as that guard remains alive and holds mutual exclusion, it confers safe mutable access (`&mut T`) to the interior data.

```mermaid flowchart LR accTitle: Rust Mutex Guard Pattern accDescr: The chain from a shared Mutex reference to an exclusive mutable reference via a MutexGuard.

A["Shared Mutex Reference<br>&'a Mutex&lt;T&gt;"] -->|".lock()"| B["Credential<br>MutexGuard&lt;'a, T&gt;"] B -->|".get() / DerefMut"| C["Exclusive Access<br>&mut T"] ```

However, wrapping the results table in a simple `Mutex` completely ruins database performance. Gjengset rejects this outright: “mutexes are terrible because well they can be ok but in this particular instance they're really bad because they force you to have sequential access if you think of our results table you either have to do a read or a write for any given unit time which is terrible because we want to do things in parallel we want this to be current and fast and new Texas are neither concurrent nor fast”.

A common alternative is a reader-writer lock (`RwLock<T>`), which mirrors Rust’s borrowing rules dynamically: it permits either an arbitrary count of parallel readers holding a read lock or a single exclusive writer holding a write lock. While this allows parallel reads, Gjengset hints that when high-frequency writes continuously interleave with constant reads, even an `RwLock` introduces intolerable contention and blocking.

[27:57](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1677s) The Pitfalls of Reader-Writer Locks on Fast Workloads

Jon examines whether wrapping a standard `HashMap` in a reader-writer lock (`RwLock`) actually solves the concurrency bottleneck for read-heavy workloads. He points to the underlying implementation of reader-writer locks in POSIX threads (`pthreads`) to explain why they fall short. To allow multiple threads to acquire the read lock concurrently, the lock implementation must internally acquire an exclusive mutual exclusion lock briefly to update reader state, releasing it right before returning the read handle.

This hidden exclusive lock fundamentally alters the performance characteristics depending on the length of the critical section. If a thread holds the read lock across a massive amount of computation, the lock acquisition overhead is negligible, and concurrency scales effectively. However, when the critical section is extremely short—such as looking up an entry in a hash map—the lock acquisition dominates the execution time. Because acquiring that internal lock is a strictly sequential operation, the program is serialized. Jon presents a benchmark graph comparing read and write throughput against the number of threads: initially, throughput increases with a few threads, but quickly plateaus and degrades as the system spends all its time contending on the lock rather than performing hash lookups.

```mermaid flowchart TD accTitle: Reader-Writer Lock Internal Bottleneck accDescr: Diagram showing how pthreads RwLock serializes reads through an internal exclusive lock.

subgraph RwLock["pthreads RwLock Read Path"] direction TB A["Reader Thread arrives"] --> B["Acquire internal exclusive lock"] B --> C["Increment active reader count"] C --> D["Release internal exclusive lock"] D --> E["Execute hashmap lookup"] E --> F["Re-acquire internal exclusive lock"] F --> G["Decrement active reader count & release"] end

H["Concurrent Reader Threads"] -->|Contention bottleneck| B ```

Jon emphasizes that benchmarking this naive setup reveals we are not measuring map performance at all: “All we're doing while holding the reader lock is doing a hashmap lookup, hashmap lookups are really fast, therefore we're basically benchmarking the performance of taking a lock.” To achieve ideal linear scalability without lock contention, he notes that they must eliminate locks entirely.

- In a pthreads reader-writer lock, acquiring the reader handle requires briefly taking an exclusive lock both when acquiring and releasing. - For short critical sections, taking the lock becomes the bottleneck, forcing concurrent threads into sequential execution. - Benchmark results show read throughput scaling modestly with very few threads, but rapidly flatlining as lock contention dominates.

---

[30:27](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1827s) Demystifying `unsafe` in Rust

To eliminate locks, the system must handle aliased, shared pointers without the compiler's built-in locking abstractions. Under normal Rust rules, shared pointers cannot grant mutable access because aliasing creates data races. Working around this requires the `unsafe` keyword. Jon acknowledges the reflexive panic this often induces among developers who assume `unsafe` throws away all language guarantees and reduces Rust to C. He reassures the audience to “please breathe, it will be okay,” explaining that `unsafe` is simply an explicit mechanism allowing operations permitted in C, specifically dereferencing raw pointers.

In safe Rust, references (`&T` and `&mut T`) come with strict lifetime and exclusivity contracts enforced by the compiler. A raw pointer (`*mut T`), by contrast, represents merely an address in memory with no attached lifetime or ownership guarantees; it could point anywhere, including an expired stack frame. The `unsafe` keyword grants the programmer permission to dereference raw pointers or cast them into exclusive references (`&mut T`), asserting an internal invariant that no other thread is concurrently accessing that data. When data races or memory corruption occur, developers know precisely where to audit because safety contracts are only bypassed within delimited `unsafe` blocks.

> “Whenever you type the unsafe keyword, Rust is saying you're now allowed to do the things you're allowed to do in C, there's nothing more or less magical to it.” — Jon Gjengset

- Raw pointers (`*mut T`) denote a memory address without lifetime or exclusivity contracts. - Dereferencing raw pointers or converting a raw pointer into a mutable reference (`&mut T`) requires an `unsafe` block. - `unsafe` isolates manual invariants to specific code sections, drastically narrowing down the regions that need auditing in the event of memory corruption.

---

[33:15](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1995s) Left-Right Concurrency and Epoch Tracking

Jon lays out the design of an alternative lock-free data structure designed for high-throughput reads (the Left-Right pattern). The structure maintains two identical hash maps—a left map and a right map—and two atomic pointers. Readers access maps exclusively via the top read pointer, while writers access maps through the bottom write pointer. Readers initially query the left map while the writer applies updates exclusively to the right map. When the writer wants to publish modifications, it atomically swaps the pointers: incoming readers immediately traverse the updated right map, while the writer applies the modifications to the left map to synchronize both copies.

```mermaid flowchart LR accTitle: Left-Right Dual Map Architecture accDescr: Flow showing readers reading from Map 1 via Read Pointer while Writer modifies Map 2 via Write Pointer before swapping.

subgraph Pointers["Atomic Pointers"] RP["Read Pointer"] WP["Write Pointer"] end

subgraph DataMaps["Internal HashMaps"] M1["Left Map (Map 1)"] M2["Right Map (Map 2)"] end

Readers["Concurrent Readers"] --> RP Writer["Single Writer"] --> WP

RP -.->|"Initially reads"| M1 WP -.->|"Initially writes"| M2 ```

A severe race condition arises if this swap is done naively: readers that began querying the left map prior to the pointer swap may still be actively traversing hash buckets when the writer begins mutating that same left map. Adding locks to detect when readers clear out would destroy read performance, so the architecture turns to epoch counters. Each reader thread maintains an independent, unsynchronized local counter that it increments on map operations. After swapping pointers, the writer observes every reader's epoch counter, waiting until each counter has incremented by at least one tick before touching the old map. Seeing an increment guarantees that a reader finished any in-flight lookup that started before the swap and must read the new pointer for subsequent operations.

However, a remaining flaw exists: an inactive reader thread that holds a handle but performs no queries will never increment its counter, causing the writer to wait indefinitely. Jon explains the solution: incrementing the epoch counter twice per operation—once immediately before beginning the read, and once immediately upon completion. If the writer observes an even epoch value, it knows the reader is completely inactive and holds no references to either map, allowing writes to safely proceed without blocking.

> “If you observe a tick in every reader after you swap the pointer, you know that all of the readers must now have seen the new value, therefore they must all now be in the right map and therefore the writer has exclusive access to the left side.” — Jon Gjengset

- The structure utilizes two internal hash maps and two atomic pointers so writers and readers operate on disjoint maps. - A naive pointer swap risks data races with pre-existing readers still traversing buckets in the old map. - Local, unsynchronized reader epoch counters let the writer verify that readers have observed the pointer swap without using locks. - Readers increment their epoch counters twice (entering on an odd count, exiting on an even count); an even counter signals to the writer that the reader is inactive.

[37:27](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2247s) Benchmarking evmap Against Reader-Writer Locks

Jon Gjengset finishes explaining the mechanics behind the double-buffered lock-free map (`evmap`). The writer only needs to observe two possible states for a reader's epoch value to know it can safely modify or reclaim a buffer: either the reader has incremented the value by one, or the value is even. An even value signifies that the reader is currently outside of the critical section and not examining the map; if it decides to read later, it will inevitably pick up the newly published pointer. Combined, these invariants mean readers and writers never acquire mutual exclusion locks. Read operations stay entirely thread-local, yet both maps remain safely accessible under full concurrency.

```mermaid flowchart TD accTitle: evmap Reader State Evaluation accDescr: Flowchart illustrating how a writer verifies whether a reader can be safely ignored based on its atomic epoch counter.

subgraph WriterCheck["Writer Inspects Reader Epoch"] Check["Examine Reader's Counter"] IsOdd["Counter is Odd?"] Increased["Incremented by 1?"] IsEven["Counter is Even?"] end

subgraph Decision["Safety Decision"] Safe["Safe: Ignore Reader<br/>(Proceed with write/swap)"] Wait["Must Wait / Retry"] end

Check --> IsEven Check --> IsOdd IsEven -->|"Not inside map read"| Safe IsOdd --> Increased Increased -->|"Advanced to current epoch"| Safe Increased -->|"Still reading old epoch"| Wait ```

When comparing this data structure against a standard reader-writer lock (`RwLock`) on identical workloads—swapping maps on every single write without batching or amortization—the throughput differences are striking. While write throughput still slopes downward as reader concurrency scales up (because the writer simply gets scheduled less frequently among competing threads), it outperforms the lock-based approach significantly. But the contrast on reads is so vast that Gjengset has to rescale the y-axis entirely just to keep both on the same slide. As Gjengset quips:

> “Can you see how much it matters to not use locks, right?”

With 16 threads, `evmap` handles 25 million read operations per second. Gjengset points out that this implementation is open-source and published as a documented Rust crate, complete with open benchmarks. The entire library relies on a remarkably tiny footprint of `unsafe`:

- Exactly 7 lines of `unsafe` code exist in the core library, exclusively used when invariants prove exclusive writer access so a raw pointer can be safely cast into a Rust `&mut` reference. - An additional 7 lines of `unsafe` are used for an optional optimization that deduplicates data between the left and right maps rather than storing two distinct complete copies.

---

[40:19](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2419s) Microbenchmarks and Production Lobsters Traces

Moving beyond isolated data-structure tests, Gjengset evaluates `evmap` inside Noria against traditional storage architectures on real-world queries. He begins with a standard vote-aggregation query common to news aggregator sites like Reddit, Hacker News, and Lobsters: counting votes for a specific entity. The benchmark compares MySQL, an unnamed commercial database designated as "System Z" due to vendor benchmarking clause restrictions, a pure memcached instance, and Noria. Memcached does not execute SQL, so it is given an unfair advantage using single-roundtrip multi-increments (`incr`) and multi-gets without persistence.

At the 95th percentile latency threshold, MySQL and System Z collapse early, topping out around 200,000 operations per second. Memcached scales to roughly 7 million operations per second. Noria, despite running a complete SQL execution layer, delivers 14 million operations per second—doubling memcached. Gjengset explains that memcached still acquires internal locks on its hash tables during read operations, whereas Noria reads directly out of an un-locked, cache-materialized `evmap`.

| System Under Test | Approx. Throughput (Vote Count Query) | Operational Characteristics | |:--- |:--- |:--- | | MySQL | ~200,000 ops/sec | Latency spikes early; standard relational execution | | System Z | ~200,000 ops/sec | Protected by strict de-anonymizing benchmark clauses | | memcached | ~7,000,000 ops/sec | In-memory, non-persistent, locks table buckets on reads | | **Noria** | **~14,000,000 ops/sec** | Fully materialized SQL cache backed by lockless `evmap` |

To demonstrate real application behavior rather than synthetic single queries, the team ran an end-to-end trace from Lobsters. Because Lobsters is open source, they collected real production traffic traces and built a workload generator to replay realistic page requests and distributions against a 16-core server. When serving full web page query sets:

- MySQL sustained roughly 1,100 page views per second in initial benchmarks. Gjengset notes that upon correcting a query test where MySQL had accidentally been given an artificially simplified query, its actual throughput dropped further, to around 800 to 900 requests per second. - Noria sustained approximately 5,000 to 6,000 page views per second on the same hardware, proving that combined dynamic caching and lockless materialization deliver massive end-to-end performance gains on realistic workloads.

---

[43:23](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2603s) Learning Rust and Working With the Borrow Checker

Having demonstrated that Noria is a fully functioning, high-throughput system rather than an academic toy, Gjengset turns to the realities of adopting Rust for large software systems. He cautions that newcomers often fail because they assume Rust is just another syntactic flavor of familiar imperative languages:

> “The first thing I want to tell you is Rust is different. You cannot go about learning Rust the same way you learn all these other languages you've learned in the past.”

Its syntax looks comfortable—resembling C, C++, or ML dialects—but treating it like C, Go, or C++ leads straight into an impasse with the borrow checker. Developers unfamiliar with the underlying memory model end up "fighting the borrow checker," shouting at the compiler without understanding why it is rejecting their design.

Gjengset insists that the learning curve is entirely about mastering borrow semantics. Rather than hacking away aimlessly, newcomers should sit down and read *The Rust Programming Language* book cover to cover, a practice rarely required for other languages but essential here. For developers who learn better through executable code, *Rust by Example* covers the same terrain through interactive, runnable samples.

Crucially, developers must learn to treat friction with the compiler as an architectural design signal. When a pattern feels unreasonably difficult, it usually means the developer is attempting an unidiomatic layout. For instance, building a cyclic graph using raw inter-object pointers—a reflex from C—forces complex reference counting or unsafe code in Rust. By contrast, reshaping the graph into a flat vector of nodes referenced by numeric array indices eliminates pointer cycles entirely and satisfies borrow rules without resistance. When compiler friction cannot be diagnosed alone, Gjengset suggests leveraging the community:

- The `#rust` and `#rust-beginners` channels on Mozilla IRC (`irc.mozilla.org`, often referred to as Moznet) provide rapid support. - Questions posted in those IRC channels are frequently answered within five minutes, often directly by Rust core team members.

[46:57](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2817s) Idiomatic Rust, Clippy, and Modular Crates

Jon Gjengset stresses the value of working with the compiler rather than fighting it. If the borrow checker can understand your code, a human teammate is vastly more likely to understand it too. Developers tempted to bypass the borrow checker by sprinkling raw pointers, complex aliasing, and `unsafe` blocks often regret it down the road: “chances are like six months later or three months later when you come back and look at this code you will not understand why you did what you did or what invariants are being enforced by the unsafe and no one else is gonna understand it either.” Avoiding `unsafe` wherever possible yields code that remains comprehensible and maintainable over time.

He encourages newcomers to master the specific idioms and patterns native to Rust. Concepts like iterators introduce functional programming techniques—reminiscent of Haskell or Python generators—that make walking data structures clean and expressive. Other indispensable language features include pattern matching, expressive error handling, and the `?` operator for streamlined error and `None` propagation. To accelerate learning, Gjengset points out an earlier blog post he wrote on these idiomatic patterns and highly recommends Clippy, the official Rust linter: “Clippy is a tool that will take your rust code and tell you all the things you probably shouldn't be doing it's like basically someone wrote a thing that's gonna do peer review for you and it's gonna yell at you for like don't do that don't do that don't do that it's great it's like `go vet` for those who used to go.”

Gjengset also urges developers to break their code into independent crates early and often. Because creating and depending on crates in Rust incurs minimal overhead, splitting projects enforces strict separation of concerns and modular architecture. Beyond modularity, modular crates bring immediate practical benefits: Cargo compiles independent crates in parallel, accelerating build times, and the crate boundary encourages isolated, comprehensive documentation. Developers can enforce this via the language-level compiler attribute `#![deny(missing_docs)]`, which rejects the compilation of any crate containing undocumented public items.

- **Rust Idioms to Adopt:** Iterators for functional traversal, robust pattern matching, and the `?` operator for terse error/null handling. - **Automated Peer Review:** Clippy provides community-driven lints catching anti-patterns before human code review. - **Crate-Driven Architecture:** Breaking large codebases into crates enables modular design, faster parallel builds, and compiler-enforced documentation using `#![deny(missing_docs)]`.

[49:32](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2972s) Compiler-Guided Refactoring and the Crates.io Ecosystem

A standout quality of Rust is the confidence and ease it brings to large-scale refactoring. Thanks to the expressiveness and rigor of the type system, engineers can fearlessly reorganize data structures and move entire chunks of logic around, allowing the compiler errors to act as a step-by-step checklist of necessary updates: “you can do compiler guided refactoring and you can basically trust the compiler this is for those of you program in Haskell in the past is a similar kind of feeling of when it compiles it's probably pretty close to correct.” While semantic logic bugs can still slip through, the type checker catches structural mismatches, missing branches, and memory-safety violations long before runtime.

Gjengset similarly highlights the strength of the Rust packaging and tooling ecosystem. Rust's dependency manager (`cargo`) makes importing and integrating external open-source crates painless. Because formatting via `rustfmt` is widely standardized and documentation on Docs.rs is automatically generated and cross-linked across crates, evaluating and reading third-party code feels seamless. If you publish your own crate, its types automatically link back to its dependencies' documentation. While the ecosystem was still maturing in late 2018, missing components simply offer opportunities to author new libraries for the broader community.

```mermaid flowchart TD accTitle: Compiler-Guided Refactoring Flow accDescr: A cyclical refactoring workflow where type changes prompt compiler errors that guide the developer until the code compiles correctly.

subgraph Refactor["Refactoring Loop"] A["Architectural / Type Changes"] --> B["Run Compiler (`cargo check`)"] B --> C{"Does it compile?"} C -- "No: Compiler reports type/borrow errors" --> D["Address Diagnostics as a Checklist"] D --> B end

C -- "Yes" --> E["Working, High-Confidence Code"] ```

[51:02](https://www.youtube.com/watch?v=s19G6n0UjsM&t=3062s) How the Borrow Checker Rewires a Programmer's Mindset

To conclude his presentation, Gjengset reads a quote reflecting on how Rust fundamentally transforms how engineers reason about concurrency and correctness across all languages:

> “One of the value propositions most frequently lauded by Rust developers is its freedom from data races. The compiler will literally not allow you to build code that could ever produce a situation where two threads can mutate the same data. This teaches us two things: loathing of the Rust compiler and its borrow checker until we make things work, and two, the extent to which we've written unsafe, race-prone code in other languages. I've learned a great deal from that second bullet point. I look back at some of the code I've written in the past and I realized the only reason it worked is because some other library had guardrails in place, or the lack of data races was an accident. In other words, my lack of failure is more attributable to random chance than skill.”

Jon emphasizes how deeply this realization resonated with his personal growth as an engineer: “I really feel like this when writing rusts code now that it has made me a better programmer.” Returning to dynamic or unconstrained systems like Python or C, he now instinctively spots latent concurrency races because he recognizes patterns the Rust compiler would outright reject. When reviewing legacy code written in prior languages, he often concludes that it is likely flawed or fragile, leaving him wishing he had the Rust compiler at hand to pin down exactly what is wrong.

He wraps up the session by summarizing that the talk aimed to bridge low-level implementation details of the Noria database prototype with high-level takeaways about Rust's engineering ergonomics. He opens the floor for questions, offering his contact details via email, Twitter, GitHub, or through the host Larry, followed by applause from the audience.

## Notable quotes > [10:58](https://www.youtube.com/watch?v=s19G6n0UjsM&t=658s) If you just want to write something and not worry about the details, Rust is an awful language for that job because Rust will force you to write correct code. — Jon Gjengset

> [07:11](https://www.youtube.com/watch?v=s19G6n0UjsM&t=431s) In Rust, lots of concurrency bugs are impossible—the compiler will not let you compile a program that has a data race in it — Jon Gjengset

> [30:27](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1827s) Whenever you type the unsafe keyword, Rust is saying you're now allowed to do the things you're allowed to do in C, there's nothing more or less magical to it. — Jon Gjengset

> [43:23](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2603s) The first thing I want to tell you is Rust is different. You cannot go about learning Rust the same way you learn all these other languages you've learned in the past. — Jon Gjengset

## Mentioned Jon Gjengset, Larry Rudolph, Two Sigma, Two Sigma Labs, MIT CSAIL, Parallel and Distributed Operating Systems (PDOS), Noria, Rust, Mozilla, C, C++, Haskell, OCaml, Go, Python, JavaScript, Ruby, Firefox, Cargo, Intel DPDK, GTK, Qt, Win32, macOS Cocoa, Memcached, Redis, Naiad, Apache Spark, POSIX threads (pthreads), Lobsters, MySQL, Reddit, Hacker News, Clippy, Docs.rs, Crates.io

## Video description This is a guest lecture I gave at Two Sigma in November 2018 where I discussed the experience of using Rust for building larger, high-performance systems. In it, I cover what makes Rust an attractive option for such projects; Noria, the high-performance research database prototype I've built using Rust; an interesting concurrent data-structure we use in Noria; and how I've found Rust to work in that context.

The presentation slides are available at https://jon.thesquareplanet.com/slides/rust-twosigma/ . You can read more about Noria at https://pdos.csail.mit.edu/noria .

[Source](https://www.youtube.com/watch?v=s19G6n0UjsM)

中文简报

麻省理工学院 PDOS Noria 数据库借助无锁 Rust 实现每秒 1400 万次操作 摒弃读写锁并采用代际跟踪的双重映射,使写驱动的数据流数据库吞吐量达到专用内存缓存的两倍。 Gjengset 解释了传统关系型数据库如何在读密集型工作负载下因重复计算查询而浪费 CPU 周期,并展示了 Noria 如何通过写驱动的增量数据流维护动态自更新的物化视图来解决这一问题。为了在不阻塞读取的情况下消除共享结果缓存上的争用,他剖析了无锁 `evmap` 数据结构背后的 Left-Right 并发模式和读取者代际机制。微基准测试与逼真的 Lobsters 生产环境链路追踪表明,在高并发场景下,Noria 的性能显著优于 MySQL、商业数据库以及 Memcached。最后,他为采用 Rust 提供了实用建议,说明了如何与借用检查器协作、利用 Clippy 等工具,并适应由编译器引导的重构。 Noria 是麻省理工学院 CSAIL 用 Rust 构建的拥有 6 万行代码的研究型数据库原型,它用写驱动的增量数据流取代了读取时的查询执行,将读取操作转变为 O(1) 复杂度的物化视图查找。 (1:21) 标准的读写锁(RwLock)在哈希映射查找等快速、短暂的临界区中表现不佳,因为 pthreads 在内部会使用互斥锁来管理读取者计数,从而使读取操作串行化。 (27:57) 通过使用带有代际跟踪的双缓冲 Left-Right 映射(evmap),Noria 在计票查询上实现了每秒 1400 万次操作,优于 memcached(每秒 700 万次操作)以及 MySQL 等关系型引擎(每秒 20 万次操作)。 (40:19) 核心的无锁 evmap 数据结构仅依赖 7 行非安全(unsafe)Rust 代码,这表明高并发系统可以将手动维护的不变量隔离在极小且可审计的代码块中。 (37:27)

Reading summary

Drawing on the development of MIT's Noria database, Jon Gjengset argues that Rust eliminates the traditional trade-offs between execution speed, memory safety, and developer ergonomics, provided engineers adapt to its ownership model rather than fight it. Rust's Core Invariants and Developer Trade-Offs (3:19) Rust enforces memory safety and freedom from data races at compile time by restricting data access to either an arbitrary number of immutable references (&T) or a single exclusive mutable reference (&mut T). This design eliminates bugs common to systems languages, such as use-after-free, double-free, and null pointer exceptions, without relying on a garbage collector. Features like zero-cost abstractions, algebraic data types (`Result` and `Option`), pattern matching, and integrated package tooling through Cargo provide high-level expressiveness alongside low-level C and C++ performance. However, Gjengset emphasizes that Rust is not universal. Its exhaustive type checking and lifetime enforcement create significant friction during rapid, disposable prototyping. Additionally, binding maintenance remains high when integrating massive vendor C frameworks like Intel DPDK, while domains such as kernel programming (`no_std`), graphical user interfaces, and early async I/O primitives continue to present ergonomic challenges. Noria's Architecture: Write-Driven Incremental Dataflow (13:35) Web application workloads are typically read-heavy, often exceeding 90% read traffic. Traditional relational databases waste CPU cycles repeatedly calculating identical joins and aggregations on reads, while external caching layers like Redis or Memcached introduce manual cache invalidation complexity and catastrophic thundering herd failures. Noria addresses this by inverting the execution model: it evaluates queries incrementally on write rather than on read. When a write occurs, updates propagate through an internal dataflow graph of relational operators that directly refresh an in-memory materialized view. Reads become simple O(1) key-value lookups against this pre-materialized state without requiring application-level cache invalidations. To remain viable beyond simple static graphs, Noria handles practical systems challenges including dynamic online schema migrations and partial materialization—evicting unused cached results and recomputing them on demand to prevent memory exhaustion. Lock-Free Results Caching via Evmap (27:57) Noria's shared materialized results table must support massive concurrent read and write operations without data corruption. Standard synchronization primitives fail under this profile: a `Mutex` serializes all operations, while a POSIX `RwLock` introduces severe contention on short critical sections because its internal implementation briefly acquires an exclusive lock to manage reader counts, degrading read throughput. To bypass locks, Gjengset implemented `evmap`, an open-source Rust library based on Left-Right concurrency. It maintains two underlying hash maps and atomic pointers. Readers access one map while the writer mutates the other. After applying an update, the writer swaps pointers and tracks per-thread reader epoch counters—where an increment or an even number confirms a reader is inactive or has moved to the new map—before reconciling the second map. Using just 14 lines of `unsafe` Rust to cast raw pointers once access invariants are proven, `evmap` achieves 25 million reads per second across 16 threads without locking. Empirical Performance and Production Traces (40:19) In microbenchmarks of a vote-counting query common to sites like Reddit or Lobsters, standard engines like MySQL and an anonymized commercial database ('System Z') plateaued around 200,000 operations per second at the 95th percentile latency mark. Memcached, using non-persistent multi-increments, reached roughly 7 million operations per second while still contending with internal table locks. Noria delivered approximately 14 million operations per second, doubling memcached while preserving full SQL evaluation. Under realistic conditions using production replay traces from the open-source Lobsters site on a 16-core server, MySQL sustained between 800 and 1,100 page views per second. By contrast, Noria sustained 5,000 to 6,000 page views per second, demonstrating that dynamic caching combined with lock-free materialized reads yields substantial end-to-end performance gains. Adapting Architecture to the Borrow Checker (43:23) Gjengset cautions that learning Rust requires unlearning habits from other languages. Treating compiler friction as a diagnostic design signal prevents anti-patterns; for example, replacing pointer-heavy cyclic graphs with index-based vector storage avoids borrow checker violations cleanly. Relying on linters like Clippy, modularizing codebases into distinct crates, and leveraging compiler errors for guided refactoring allows teams to manage complex codebases with high confidence. Ultimately, working within Rust's static constraints rewires how engineers reason about concurrency in other languages, exposing how often multithreaded code in C or Python relies on accidental success rather than verified correctness. Gjengset concludes that while Rust enforces a steep learning curve and strict compile-time rigor, its ownership model makes it exceptionally viable for building complex, high-concurrency infrastructure like Noria, replacing runtime synchronization bottlenecks and manual safety auditing with compiler-enforced correctness.

精炼阅读摘要

MIT CSAIL 研究员 Jon Gjengset 在 Two Sigma Labs 的演讲中指出,现代高并发系统无需在运行速度与内存安全之间做出妥协;通过将 Rust 的静态所有权检查机制与基于写入期增量数据流的 Noria 数据库架构相结合,工程团队能够消除外部缓存失效与锁争用难题,在保证绝对内存安全的前提下获得突破性的并发吞吐量。 Rust 工程特性:零成本安全与开发边界 (3:19) Gjengset 指出,传统系统级编程长期受困于性能与可靠性的两难抉择,而 Rust 试图通过表达性类型系统、零成本抽象与借用检查器打破该妥协。其核心所有权模型规定每个值具有唯一所有者,访问权限严格划分为排他可变引用(&mut T)与共享不可变引用(&T),两者在编译期严格互斥,从而彻底阻断数据竞争、悬垂指针与二次释放。代数数据类型(Result 与 Option)以及穷尽性模式匹配在零运行时开销下规范了异常处理与空值解构,使未捕获错误在编译阶段无所遁形。 然而,Gjengset 明确指出 Rust 并非万能方案。当系统需要与庞大且特化的 C 框架(如 Intel DPDK)交互时,封装与维护绑定的成本极高;在快速构建一次性丢弃型脚本或粗糙原型时,编译器严苛的错误处理与生命周期校验亦会显著增加摩擦。此外,无标准库(no_std)的底层内核开发、与单所有权模型难以贴合的桌面 GUI 体系,以及人体工程学仍在持续演进的异步 I/O 生态,均属于 Rust 当时仍待完善的应用领域。 Noria 架构:写入期增量流替代外部缓存 (13:35) 现代 Web 应用普遍呈现超过 90% 读取的读密集特征,传统关系型数据库在每次读取时重复执行 SQL 解析、聚合与表连接,耗费了大量 CPU 周期。尽管业界普遍采用 Memcached 或 Redis 作为外部缓存层,但这将繁琐且易出错的缓存失效逻辑转移至应用层,并极易引发缓存失效瞬间压垮后端的惊群效应(thundering herd)。 Noria 颠覆了这一模式,主张由数据库自身维护缓存。应用注册 SQL 查询后,数据库内部会构建包含过滤、计数与连接算子的持久化数据流图。底层基础数据更新时,Noria 仅在写入期沿数据流图增量推导状态变化,并将最终计算结果推送至内存中的物化视图(Materialized View)。前端读请求因此被转化为针对物化视图的高速 O(1) 索引检索,免去了应用级缓存维护。针对有限物理内存,Noria 引入部分物化(Partial Materialization)机制以按需重新计算历史状态,并支持在线动态变更数据流拓扑而无需停机重置。 突破锁瓶颈:Left-Right 与无锁结构 (27:57) 在 Noria 的结果物化表中,海量并发读请求与高频写入流在此交汇。Gjengset 分析表明,传统的互斥锁(Mutex)会导致并行访问彻底串行化,而基于 POSIX 线程(pthreads)的标准读写锁(RwLock)在极短的哈希表查找中同样表现糟糕:因为读写锁在获取与释放读锁时必须短暂获取内部排他锁以更新读者计数,这一同步开销直接支配了执行时间,导致吞吐量随线程数增加迅速饱和恶化。 为彻底消除锁开销,Gjengset 团队基于 Left-Right 模式设计了无锁并发映射表 evmap。该结构维护左右两个完全相同的哈希表以及独立的读、写原子指针。读者持续访问左表,写者则修改右表;更新发布时,写者原子交换指针,并通过局部未同步的读者纪元计数器(Epoch Counter)追踪读者状态。读者在每次操作前后各递增一次计数器,写者若观察到偶数计数器则知晓该读者处于临界区外,从而确保旧缓冲区在无互斥锁的情况下安全同步。整个核心实现仅包含 7 行 unsafe 代码用于断言排他写入不变量,在 16 线程下实现了每秒 2500 万次读取。 实测性能验证:微基准到生产全链路表现 (40:19) 在模拟类似 Reddit 或 Hacker News 实体点赞计数的聚合查询微基准测试中(95 分位延迟阈值下),传统关系数据库 MySQL 与某匿名商业数据库(代号 System Z)在达到约每秒 20 万次操作时即遭遇瓶颈。纯内存系统 Memcached 借助多键批量递增虽可达到每秒约 700 万次操作,但其内部哈希桶仍存在读锁开销。相比之下,运行完整 SQL 执行层的 Noria 依靠无锁 evmap 物化缓存,吞吐量达到每秒 1400 万次,达到 Memcached 的两倍。 在更严苛的生产环境端到端验证中,研究团队利用开源社区 Lobsters 的真实生产流量追踪轨迹进行了重放测试。在一台 16 核服务器上服务整套复杂 Web 页面查询时,MySQL 在纠正简化查询偏差后的实际吞吐量仅为每秒 800 至 900 次请求;而 Noria 则在相同硬件上稳定维持在每秒 5000 至 6000 次请求,证明了动态增量缓存与无锁物化在真实工业级混合负载下的巨大性能优势。 适应借用检查器:工程落地与认知重塑 (43:23) Gjengset 强调,掌握 Rust 的关键在于改变思维模式,而非将其当作另一门命令式语言。当开发者感觉“在与借用检查器对抗”时,通常意味着系统设计违背了 Rust 的安全模型。例如,在 C 中惯用的原始指针循环图结构在 Rust 中极其繁琐,但若重构为由扁平数组索引引用的紧凑节点向量,即可在免除指针周期的同时轻松通过借用检查。团队应积极采用官方静态分析工具 Clippy 拦截反模式,并尽早拆分模块化 Crate 以加速并行编译和文档检查。 此外,Rust 的类型系统赋予了开发者进行“编译器驱动重构”的高度信心:在重组大型代码库时,编译器报错列表可直接作为迁移任务清单,几乎杜绝了运行时结构错位与竞态逃逸。Gjengset 总结道,Rust 彻底重塑了工程师对并发安全的心智模型,促使开发者意识到过去在其他语言中未崩溃的代码往往仅是依赖巧合,而 Rust 编译器的强制约束则将这种侥幸转化为经过静态证明的代码确定性。 尽管 Noria 证明了增量流处理与无锁物化在极端并发下的性能潜力,Gjengset 亦指出该方案在部分物化策略、内存淘汰以及动态演进算法上引入了非同寻常的实现复杂度;工程团队采纳 Rust 时必须跨越借用语义的陡峭学习曲线,并理性评估在特化硬件驱动与异步人体工程学方面的生态约束。

Detailed notes — zh

## Background 该视频由 Jon Gjengset 于 2019 年 1 月 5 日发布在 YouTube 频道上。视频记录了他在 2018 年 11 月受邀前往量化投资公司 Two Sigma 进行的一场技术客座讲座,页面简介说明其内容主要探讨使用 Rust 构建大规模、高性能系统的实践经验,涵盖 Rust 语言优势、基于 Rust 构建的高性能研究型数据库原型 Noria,以及专为其研发的并发数据结构。 ## TL;DR Jon Gjengset 首先梳理了 Rust 的核心优势与适用边界,剖析其如何借助所有权系统、借用检查器与类型系统在编译期杜绝数据竞争并实现“无畏并发”。随后,他深入介绍了自己在 MIT 历时三年研发的动态数据流数据库 Noria,阐明其如何通过将查询计算前置到写入期来打破传统读密集型场景下的开销瓶颈。为了解决缓存物化结果表的高并发访问难题,讲座详细剖析了开源无锁数据结构 evmap 的底层演化——从读写锁的短临界区瓶颈到基于双缓冲、Epoch 计数器与奇偶状态的极简无锁同步设计。最后,Jon 结合近 6 万行代码的实战经验,向工程师传授了与借用检查器顺应协作、克制使用 unsafe、模块化拆分 crate 以及拥抱编译器引导重构的工程心智。 ## Atmosphere & style 整场演讲呈现出一种严谨扎实却又生动风趣的高阶工业界与学术界碰撞氛围。Larry Rudolph 的幽默捧场引出 Jon Gjengset 极富亲和力且逻辑严密的硬核输出,讲者在黑板式的白板图解与实测基准图表间切换自如。每当触及 unsafe、多线程数据竞争等令底层工程师神经紧绷的话题时,Jon 都能用风趣的安抚化解焦虑,同时以极其透彻的白话剖析底层锁机制与硬件交互,带来极强的技术顿悟感与思维重塑冲击。 ## Bulletin - [01:21](https://www.youtube.com/watch?v=s19G6n0UjsM&t=81s) MIT CSAIL PDOS 团队耗时三年用 60,000 行纯 Rust 代码打造了新型研究数据库 Noria,通过在写入期增量驱动数据流图并维护物化视图缓存,彻底颠覆了传统关系型数据库在读取期重复执行昂贵查询规划的范式。 - [37:27](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2247s) Noria 核心结果表采用基于双缓冲和 Epoch 代际计数器的无锁并发数据结构 evmap,通过仅 7 行 unsafe 代码实现了读者与写者完全免除锁争用,在 16 线程下微基准读吞吐达每秒 2500 万次。 - [40:19](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2419s) 在针对 Lobsters 开源社区的真实生产流量压测中,基于 evmap 缓存的 Noria 吞吐量达到了每秒 5,000-6,000 次请求,超越传统 MySQL 达 6 到 7 倍,并在简单计数场景下跑出了纯内存 Memcached 两倍的性能。 - [43:23](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2603s) Jon Gjengset 总结指出学习 Rust 绝不能与借用检查器对抗,拆分细粒度 crate 不仅能提升构建效率,更可借助 Rust 强大的类型系统进行完全信赖编译器的引导式架构重构。 ## Key takeaways - [03:18](https://www.youtube.com/watch?v=s19G6n0UjsM&t=198s) Rust 打破了传统系统编程中极致性能与内存安全不可兼得的铁律,在提供媲美 Haskell 的强大类型系统的同时实现零运行时开销抽象与无缝 C/C++ FFI 互操作。 - [09:20](https://www.youtube.com/watch?v=s19G6n0UjsM&t=560s) Rust 拥有出色的现代化内置工具链(Cargo)和自动提取代码运行的文档测试系统,但在快速原型开发、绑定库缺失的硬件底层及未成熟的 GUI 领域仍具局限性。 - [13:35](https://www.youtube.com/watch?v=s19G6n0UjsM&t=815s) 传统 Web 架构在关系型数据库前架设 Redis/Memcached 缓存层,不仅带来双写一致性和缓存穿透的复杂性,在热点数据更新时极易引发导致数据库崩溃的惊群效应(Thundering Herd)。 - [16:09](https://www.youtube.com/watch?v=s19G6n0UjsM&t=969s) Noria 的核心设计理念是将计算从读取期移至写入期,通过持久化的动态数据流图(Dataflow Graph)自动增量更新物化视图,使复杂的 SQL 读取直接退化为单次内存点查。 - [18:37](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1117s) 构建类似 Noria 的增量流数据库面临部分物化状态驱逐、查询拓扑运行时动态演进,以及共享结果表高并发读写三大核心系统挑战。 - [21:01](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1261s) Rust 所有权机制通过严格保证任何资源在同一时刻要么拥有多个不可变共享借用(&T)、要么拥有唯一独占可变借用(&mut T),在编译期从物理层面消除了数据竞争。 - [27:57](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1677s) 在哈希查找等短临界区场景下,使用读写锁(RwLock)获取读句柄时内部仍需竞争排他互斥锁,导致并发读退化为串行争用,无法随核心数线性扩展性能。 - [30:27](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1827s) Rust 的 unsafe 关键字本质上是赋予开发者解引用裸指针的能力,将排他性保证的责任从编译器转移至开发者,但将潜在 bug 的审计范围缩小至极小的受控代码段内。 - [33:15](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1995s) 双缓冲无锁数据结构 evmap 通过让读者和单写者分别操作左右两张表,并在原子交换指针后依靠线程本地的 Epoch 计数器安全确认旧表中残留读者的退出。 - [34:47](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2087s) 为化解休眠读线程造成的死锁问题,evmap 引入双重自增与奇偶状态机:读者在临界区内 Epoch 为奇数、空闲期为偶数,写者遇到偶数状态即可直接安全推进更新并回放写操作。 - [43:23](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2603s) 初学者编写 Rust 时常因沿用 C 风格裸指针与循环引用而陷入与借用检查器的搏斗,地道做法是将实体保存在 Vec 中并通过数组索引维护关系。 - [46:57](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2817s) 过度滥用 unsafe 与指针别名会破坏代码的不变量可读性,倡导运用迭代器、问号运算符模式匹配以及静态分析工具 Clippy 编写地道 Rust 代码。 - [48:44](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2924s) 将大型项目拆解为细粒度的 crate 能够强制解耦架构、加速并行编译,结合开启 #[deny(missing_docs)] 规范文档,实现完全由编译器报错指引的无畏架构重构。 - [50:14](https://www.youtube.com/watch?v=s19G6n0UjsM&t=3014s) 长期编写经过借用检查器形式化验证的 Rust 代码能深刻重塑程序员的思维模型,彻底根除过去在其他语言中依靠侥幸运行而非技术规范所编写的并发竞争隐患。 ## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=s19G6n0UjsM&t=0s) Larry Rudolph 的开场介绍与 Jon Gjengset 的学术背景 来自 Two Sigma Labs 团队的 Larry Rudolph 为演讲作开场介绍。他提到 Labs 团队的目标之一就是联结学术界,而这次演讲非常特别。Larry 与同事 Trevor 前往麻省理工学院计算机科学与人工智能实验室(MIT CSAIL)探讨固件安全问题时萌生了使用 Rust 的想法,并由此结识了 Jon Gjengset。Larry 打趣说 Jon 是“MIT 的 Rust 专家、美东地区的 Rust 专家,甚至是全球的 Rust 专家”。与此同时,Jon 的研究课题聚焦于数据库、新型数据库架构、并发协调与冲突处理等 Larry 非常感兴趣的方向。因此在邀请 Jon 前来演讲时,Larry 提出的要求是他必须同时覆盖 Rust 和新型数据库两方面内容,而 Jon 爽快答应了。 Larry 还梳理了 Jon 的学术经历:在澳大利亚完成本科,随后前往英国伦敦,又从挪威一路辗转,最终来到麻省理工学院 CSAIL 的并行与分布式操作系统组(PDOS)攻读博士学位。讲座之后还将安排深度研讨与扩展问答环节供现场工程师交流。 - **演讲者背景**:Jon Gjengset,MIT CSAIL PDOS 实验室博士生。 - **求学轨迹**:澳大利亚(本科) $\to$ 英国伦敦 $\to$ 挪威 $\to$ 美国 MIT。 --- ### [01:21](https://www.youtube.com/watch?v=s19G6n0UjsM&t=81s) Noria 数据库项目与本场讲座的路线规划 Jon 接过话筒,说明了讲座的主题:一方面整体介绍 Rust 语言,另一方面重点分享他在 MIT 历时三年研发的系统研究原型——Noria。Noria 是一款高并发数据库,其设计思路与传统数据库截然不同。Jon 希望通过这个项目向大家传递使用 Rust 构建大型真实工程系统的体验,因为这正是许多工程师对 Rust 抱有疑虑的地方。Noria 目前包含约 60,000 行 Rust 代码,由研究团队耗时三年打磨。此外,Jon 长期维护多个 Rust 开源库,深度参与 Rust 异步 I/O 的生态建设,并在网上进行了大约 30 小时的实时 Rust 编程直播。 ```mermaid flowchart TD accTitle: Lecture Structure accDescr: High-level overview of topics included and excluded in the lecture subgraph Covered["本次讲座重点涵盖"] RUST_HIGH["Rust 高层特性<br/>所有权系统与无畏并发"] NORIA_ARCH["Noria 整体架构<br/>流式与缓存结合"] DATA_STRUCT["专用并发数据结构<br/>针对数据库场景定制"] end subgraph NotCovered["本次讲座暂不展开"] SYNTAX["Rust 基础语法细节"] QUERY_OPT["深度查询执行与规划优化"] end ``` 在界定讲座范围时,Jon 强调今天不会深陷基础代码语法,但会探讨 Rust 标志性的“所有权”(Ownership)机制——这是初学者普遍觉得具有挑战性但也最核心的设计。讲座将展示 Noria 的整体架构,并重点拆解为其专门设计的一款高并发数据结构;而深度的查询执行与查询规划优化等数据库底层细节则建议大家查阅相关论文或在会后交流。 - **代码体量**:Noria 项目拥有约 60,000 行纯 Rust 代码。 - **开源贡献**:Jon 深度参与异步 I/O 建设,并贡献过多项进入 Rust 编译器主线的语言特性。 --- ### [03:18](https://www.youtube.com/watch?v=s19G6n0UjsM&t=198s) 什么是 Rust:“快、可靠、高生产力,三者兼得” Jon 探讨了为何要关注 Rust 这门由 Mozilla 主导、在演讲当时达到 1.0 版本约四年的系统级编程语言。“系统编程”(Systems Programming)虽然定义相对宽泛,但本质上是用来构建驱动现实业务运转的系统基础设施——例如服务器、高性能客户端以及底层中间件,而非仅仅是简单的命令行小工具。针对传统编程语言常常迫使开发者在关键特性间做妥协(例如 C 语言拥有极致性能却无法保障内存安全),Rust 官方提出了著名的口号:“快速、可靠、高生产力:三者兼得”。 “Rust wants to get a do away with those trade-offs instead saying let's try to do all of them in particular Russ takes this approach with concurrency so in rust they have the slogan of fearless concurrency you should be able to write code that is concurrent without worrying that you have data races than your code is gonna crash all the time.”(Jon Gjengset 解释道) Rust 的演进模式高度依赖社区。其开发完全基于 GitHub 开源运作,拥有开放的 RFC(Request for Comments)机制来审议语言与编译器的扩展提案,并保持严格的 6 周定期发布周期,新特性能够迅速交付给开发者。 - **核心主张**:打破传统性能与安全互斥的铁律,推行“无畏并发”(Fearless Concurrency)。 - **迭代节奏**:基于 RFC 流程推进,遵循 6 周为一个稳定版本的发布周期。 --- ### [05:01](https://www.youtube.com/watch?v=s19G6n0UjsM&t=301s) 核心特性:强大的类型系统、零成本抽象与 C 互操作性 面对“为什么不选 Haskell、OCaml 或 Go”的疑问,Jon 指出了 Rust 的独特技术支柱: 1. **类型系统与零成本抽象**:Rust 的类型系统表达能力堪比 Haskell,远超 C 语言,能让开发者通过类型严格约束系统状态;同时它提供多态与接口抽象,编译器在单态化(Monomorphization)后生成的机器码毫无运行时性能损耗,兼具抽象与 C 级别的原始执行效率。 2. **借用检查器(Borrow Checker)**:在编译期间从根源上杜绝数据竞争(Data Races),消除了运行时的此类风险。 3. **出色的工业级工具链**:汲取了过去数十年来编程语言与工程基建的经验教训,现代化工具链(如 Cargo)极为开箱即用。 4. **与 C/C++ 的直接互操作性(FFI)**:与许多独立生态语言不同,Rust 从一开始就将与 C/C++ 的无缝互操作作为核心目标。这意味着团队无需推倒重来,可以精准替换现有遗留系统或库中的关键性能模块。Mozilla 当初正是出于这一目的研发 Rust,将 Firefox 浏览器中的关键组件逐步重写为高并发且内存安全的 Rust 模块。 不仅适用于底层系统开发,Jon 发现 Rust 的高层表现力同样出色。得益于类型推导与现代语法设计,编写 Rust 的体验有时接近 Python 的简洁流畅,但又能同时坐享编译期强类型检查和极高的运行性能,无论是写高层网络微服务还是写基础设施 CLI 工具都非常顺手。 --- ### [07:11](https://www.youtube.com/watch?v=s19G6n0UjsM&t=431s) 编译期安全保障与代数数据类型(Algebraic Data Types) Jon 进一步切入 Rust 具体的技术细节,指出该语言最震撼其他语言背景开发者的一点:大量传统并发与内存 Bug 在编译期就被物理消除了。在 C 语言中,程序写完跑崩是家常便饭;而在 Rust 里,不仅数据竞争(Data Races)在编译期被拦截,诸如“释放后使用”(Use-After-Free)和“双重释放”(Double Free)等内存杀手在所有权系统的保证下也根本无法编译通过。 “In rust lots of concurrency bugs are impossible the compiler will not let you compile a program that has a data race in it and if you come from basically any other language this is very surprising right in C you will write a program and you expect it to crash at runtime the first few times do you run it this is not the case with rusts code.”(Jon Gjengset 强调) 在异常与状态表达上,Rust 摒弃了传统的 null 或易错的多返回值模式,采用了代数数据类型(Algebraic Data Types, ADT),即带负载数据的强类型枚举(Enum): - **错误处理:`Result<T, E>` 枚举**:与 Go 语言返回 `(value, err)` 元组并依赖程序员手动判断 `err == nil` 不同,Rust 强制使用包含 `Ok(T)` 与 `Err(E)` 的泛型 `Result`,迫使开发者显式处理异常分支,杜绝了遗漏错误处理的隐患。 - **空值安全:`Option<T>` 枚举**:完全抛弃 null 指针隐患,采用 `Some(T)` 和 `None` 来表征可选值。 - **零运行时开销**:这些抽象机制在运行时均不产生额外的性能惩罚(例如 Option 的空指针优化)。 - **模式匹配(Pattern Matching)**:宛如“打了类固醇的 switch 语句”,不仅能解构枚举包裹的值,还能进行极其复合的守卫条件匹配(例如同时验证计算成功、数值处于 4 到 17 之间且附带字符串满足特定条件),使得分支逻辑清晰、严密。 ### [09:20](https://www.youtube.com/watch?v=s19G6n0UjsM&t=560s) Rust 的开发生态:内置包管理、文档测试与卓越的 FFI Jon Gjengset 继续探讨 Rust 的工程体验,指出 Rust 在高层语法表现上兼具现代感,写起来甚至带有一点 Python 或 JavaScript 的味道,而绝非冷冰冰的底层 C 代码。更引人注目的是其内置工具链:Cargo 提供了开箱即用的依赖管理机制,形式上类似 JavaScript 的 npm、Python 的 PyPI,或者 Go 语言的 `go get`,但实际能力要高级得多。开发者可以无缝混用本地路径依赖(path dependencies)、Git 仓库依赖以及中央制品库(crates.io)中的公共依赖,所有能力均原生内建,运作极为顺畅。 ```mermaid flowchart LR accTitle: Rust Tooling Workflow accDescr: 展示Rust内置工具链在依赖管理、文档测试以及外部语言互操作性方面的核心能力 subgraph DP["Cargo 依赖解析"] P1["本地路径 Path"] P2["Git 仓库"] P3["crates.io 中央库"] end subgraph DOC["文档与测试"] D1["/// 文档注释"] --> D2["自动提取代码示例"] D2 --> D3["自动编译为集成测试"] D1 --> D4["docs.rs 自动交叉互联"] end subgraph FFI["跨语言互操作"] F1["C / C++ 库"] <--> F2["Rust 核心逻辑"] F3["Python / Ruby / Go"] <--> F2 end ``` Rust 的文档系统也是语言与生态的一大亮点。只要在函数、模块甚至常量上方敲下三个斜杠(`///`)而非普通的双斜杠,注释内容就会被编译器识别为官方文档。最为关键的是,文档中的所有示例代码都会被自动提取并作为集成测试运行,从根源上杜绝了示例代码过时失效的隐患。在上传 crate 到公共仓库后,系统会自动为其构建排版规范、样式统一且带超链接交叉索引的在线文档。谈及与其他 crate 的交互与文档体验时,Jon 甚至强调:“It is actually fabulous to use and I do not use that term lightly.”(用起来简直妙不可言,我平时可不会轻易用这个词)。此外,Rust 拥有极佳的外部函数接口(FFI),无论调用现成的 C/C++ 库,还是嵌入到 Python、Ruby、Go 中,互操作都非常轻松,能够毫无阻碍地融入大型遗留系统。 --- ### [10:58](https://www.youtube.com/watch?v=s19G6n0UjsM&t=658s) Rust 目前的短板:高度定制化底层、快速原型与生态未熟领域 尽管优势明显,Jon 坦率地指出了 Rust 在当前阶段并不适用的若干场景。首先是针对高度专用化、硬件深度绑定的系统(例如 Intel DPDK)或体量极其庞大的第三方 vendor 库。如果缺乏现成的 Rust 封装,开发者必须亲自为其编写或生成庞大的 C 绑定层,维护成本极高,尽管社区在 DPDK 和 AWS EC2 等基础设施库上的支持正在逐步赶上,但目前仍非坦途。 其次,Rust 极其不适合“快速原型开发”(rapid prototyping)。如果一个任务只需要写出几行临时脚本、跑一次就扔,即便崩溃也无所谓,那么用 Rust 会痛苦万分。Jon 解释道,Rust 编译器在安全性上极为执拗:“If you're gonna write code and it's gonna compile, it shall not have data races like you are not allowed.”(如果你打算写代码并且想让它编译通过,它就绝对不准出现数据竞争,这是铁律)。其严格的类型系统强迫开发者显式处理所有错误分支、解包空值(Option/Result),这种严谨与草率的原型开发诉求完全背道而驰。 除上述两点外,Rust 目前在以下细分领域也面临挑战: - **内核模块开发**:由于内核环境中缺乏标准库(Lib C / std),虽有前沿探索,但总体依然十分棘手(finicky)。 - **GUI 桌面应用开发**:图形界面编程本身就极具挑战,目前针对 GTK、Qt、Win32 及 macOS 的绑定库仍显稚嫩。 - **异步网络与并发计算**:虽然 Rust 引入了类似 JavaScript Promise 的异步范式并在 Noria 中展现了极致性能,但在 2018 年底,高层语法抽象尚未完全补齐,底层写法依然显得冗长且较为棘手(nasty)。 --- ### [13:35](https://www.youtube.com/watch?v=s19G6n0UjsM&t=815s) 传统数据库的读开销陷阱与外挂缓存引发的“惊群效应” 演讲的主题正式切换到核心项目:高性能研究型数据库 **Noria**。Jon 从日常业务中极为普遍的场景切入:一个典型的社交资讯系统包含 `stories`(文章)与 `votes`(投票)两张表,前端经常需要查询特定文章的标题及其累计得票数: ```sql SELECT stories.title, COUNT(votes.id) FROM stories JOIN votes ON... WHERE stories.id =? ``` 在传统关系型数据库(RDBMS)中,每当这类查询到达,数据库优化器都会经历一套完整的查询规划(Query Planning):先对投票数据执行聚合计数,再与文章表执行关联(Join),最后进行过滤并返回。Jon 犀利地指出,绝大多数现代 Web 服务的瓶颈在于:绝大部分操作都是读操作(Selects),而每一次读取都在重复执行相同的计算。即使底层数据根本没有更新,数据库依然在源源不断地空耗 CPU 周期。 ```mermaid flowchart TD accTitle: Traditional Database Bottleneck vs Cache Pitfall accDescr: 对比传统关系型数据库昂贵的读取规划与引入外挂缓存后引发的惊群效应隐患 subgraph TRAD["传统数据库模型"] Q["客户端读取请求"] --> QP["每次重新执行查询规划"] QP --> C["聚合计算 Count"] C --> J["数据关联 Join"] J --> R["返回结果"] end subgraph CACHE["外挂缓存架构缺陷"] W["并发写入/失效缓存"] --> M["键失效 (Cache Invalidation)"] M --> TH["热门文章缓存击穿"] R2["海量并发读请求"] --> TH TH -->|"惊群效应 (Thundering Herd)"| DB["数据库负载爆表崩溃"] end ``` 许多工程团队习惯于在数据库前架设 Redis 或 Memcached 作为缓存层,但这种方式带来了棘手的架构复杂性。业务代码不仅要小心翼翼地处理缓存失效,还必须防范缓存过度失效(Over-invalidation)。更为致命的是分布倾斜带来的“惊群效应”(Thundering Herd):当一篇热门文章发生更新写入时,应用程序立刻将该热门键从缓存中剔除;在微秒之间,随之而来的海量高并发读取全部发生缓存穿透,排山倒海般直接轰击底层数据库,瞬间造成数据库宕机。Jon 提到,Facebook 曾专门发表过论文来详述其 MySQL 与 Memcached 架构在应对该问题时的极端复杂缓解方案。Jon 总结道:“All of this seems like so much complexity in order to make your application fast.”(为了让应用跑得快一点,竟然搞出了这么一大堆复杂的妥协)。 --- ### [16:09](https://www.youtube.com/watch?v=s19G6n0UjsM&t=969s) Noria 的破局之道:将计算移至写入期的增量数据流视图 面对缓存维护的泥潭,Noria 提出了一个颠覆性的直觉假设:“既然数据库本身完全知晓你的所有查询意图,为什么不能由数据库自己来维护缓存?” 传统数据库是“在读取时执行查询计算”,而 Noria 彻底颠覆了这一范式——它采用**在写入时增量执行计算(execute the query on write incrementally)**。在 Noria 中,查询在初始注册后,系统会构建一个持久化的**动态数据流图(Dataflow Graph)**,并在图的终端构建对应查询的物化视图(Materialized View),该视图本质上就是由数据库内核管理的确定性缓存。 ```mermaid flowchart LR accTitle: Noria Incremental Dataflow Architecture accDescr: Noria在写入时驱动数据流图并增量更新物化视图缓存的流程 W["写入操作: 针对文章 A 新增 1 票"] --> O1["Count 算子<br/>状态由 41 增至 42"] O1 -->|"发出增量增益记录 (A, 42)"| O2["Join 算子<br/>从 stories 表关联标题 'foo'"] O2 -->|"发出复合记录 (A, 'foo', 42)"| MV["物化视图 / 内存缓存<br/>原地覆写更新 Key A: 42"] R["客户端并发读取请求"] -.->|"单次直接内存点查 (无计算开销)"| MV ``` Jon 通过具体的流转过程拆解了该机制: 1. **初始状态**:视图中记录着文章 `A` 的标题为 `foo`,当前票数为 `41`。 2. **增量写入**:一条针对文章 `A` 的全新投票到达系统,立刻注入数据流图中的 `COUNT` 聚合算子。 3. **状态递增**:`COUNT` 算子检测到旧值为 41,将其原子性递增为 42,随即向下游算子发射一条差量事件:`key A 的计数值更新为 42`。 4. **下游关联**:该事件进入 `JOIN` 算子,算子从关联的 `stories` 表中检索出对应的标题 `foo`,组合成完整记录 `(A, 42, "foo")` 并继续下推。 5. **原地覆写**:流转终点的物化视图发现已存在键 `A`,直接原地覆写其数值为 42。 在此机制下,所有后续到来的读请求只需针对物化视图执行单次内存哈希查表,其速度与读 Redis 无异,而应用层代码彻底从缓存失效与穿透处理中解放出来。数据库内部始终维持着自洽的强一致性状态。然而,Jon 随即话锋一转指出,作为一个前沿学术研究项目,这也带来了重大的工程挑战——最首要的问题就是:系统绝对不可能无限度地把所有历史查询的所有全量计算结果永久缓存在内存中。 ### [18:37](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1117s) Noria 的三大系统挑战:部分物化、动态演进与高并发同步 在构建基于数据流(dataflow)的高性能数据库 Noria 时,设计上会面临三大核心挑战。第一个挑战是内存限制与缓存淘汰。现代系统的内存终究是有限的,必须有机制将数据从缓存中驱逐出去;但在数据流设计中,这绝非易事。例如,系统缓存了文章 A 的所有聚合计算结果,一旦因内存紧张将这些结果丢弃,随后又来了一条针对文章 A 的新投票,此时负责计数的算子(count operator)根本不知道原先的投票数是多少,也就无法直接递增并输出新的计数值。因此,系统必须提供一种机制,保证即使数据曾被丢弃、或者应用发起了一个此前从未查询过的请求,系统也能动态地计算或重新计算出结果。Jon Gjengset 指出:“This is the problem of partial materialization that we will not get into in this talk, but it is a really interesting problem.”(这就是部分物化问题,本次演讲中我们不会展开,但这确实是一个非常有趣的问题。) 第二个挑战在于查询随时间的演进。现实中的应用程序并非只发起单一查询,更不会保持查询结构一成不变。随着业务成长,应用需要修改旧查询或添加全新的查询,而数据库必须对此做出响应。传统的流处理系统(如 Naiad,乃至 Spark)通常无法应对运行时发生的模式与拓扑变更;遇到新查询,往往只能将系统推倒重来,重新启动一个包含新查询集的新实例。Noria 则能在运行期直接动态适应这些变更,其实现细节收录在相关研究论文中。 第三个挑战——也是本次演讲重点剖析的核心——则是结果表(results table)的并发读写。在 Noria 中,结果表既是所有写入流最终沉淀的缓存,又是承接外部高频查询的读取端点。大量的读取者和写入者争相访问同一个结果表,一旦某个写入者在某条记录正在被读取的同时去更新它,就会产生严重的数据冲突。系统必须在读写双方之间建立极速的同步机制,而为了设计出应对该场景的特殊并发数据结构,首先需要回到计算机科学的基本原则,理解 Rust 的核心基石。 ```mermaid flowchart TD accTitle: Noria Three Core Challenges accDescr: 展现 Noria 在实际生产设计中面临的三个核心挑战:部分物化、动态查询变更与结果表高并发读写。 subgraph Challenges["Noria 的三大核心系统挑战"] C1["挑战 1:部分物化<br/>(Partial Materialization)<br/>缓存淘汰后如何重新计算状态"] C2["挑战 2:拓扑动态演进<br/>(Dynamic Queries)<br/>避免推倒重来,支持运行时增改查询"] C3["挑战 3:结果表高并发访问<br/>(Concurrent Access)<br/>大量读者与写者同时操作共享缓存"] end C1 -.->|"详见论文"| Paper["Noria 研究论文"] C2 -.->|"详见论文"| Paper C3 ==>|"本讲座重点突破"| Core["设计高效并发数据结构"] ``` ### [21:01](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1261s) Rust 所有权体系、借用规则与无数据竞争保证 为了阐明高并发数据结构的设计,必须先理解 Rust 的所有权(ownership)系统。Rust 的所有权思想与 C++ 中的 RAII 等资源管理理念类似:如果你拥有某项资源(如一块内存),你就拥有释放它的责任;但在 Rust 中,这一概念被进一步延伸:所有者有权决定“谁能以何种方式访问该资源”。对于任意类型 `T`(无论是一个字符串、数字还是数组),在 Rust 中主要存在三种表述形式: 1. `T`:拥有该值的完全所有权,并在其离开作用域时负责释放内存。 2. `&mut T`:对 `T` 的可变引用(mutable reference)。它相当于其他语言中的指针,但附带强力的语言契约:只要你持有该可变引用,就拥有对该值的绝对独占访问权(exclusive access),在持有期间绝无任何其他人可以读取或写入它;借用者只负责使用,不承担释放责任。 3. `&T`:对 `T` 的不可变引用(immutable reference)。它同样是一个指针,但约束持有者绝对不能修改 `T`,因为同一时刻可能有其他多个读者在并发读取。 Rust 编译器在编译期就会对这套契约进行严苛的形式验证,检查每一个变量与资源:绝不允许同时存在一个可变引用与一个不可变引用,也绝不允许同时存在多个可变引用。但多个不可变引用(并发只读)是完全被允许的。你可以把编译过程理解为编译器在强制程序员证明自己的代码没有违反这些互斥规则。Jon Gjengset 总结道:“And it turns out that's all you need to guarantee the program has no data races right, if you never have something modify a thing while it's being read or modified you cannot have data races because you will either just have multiple readers or you'll have a single writer which is fine.”(事实证明,仅凭这一点就足以保证程序不存在数据竞争。如果你永远不允许某个数据在被读取或修改的同时发生变更,你就不会产生数据竞争,因为你要么拥有多个读取者,要么拥有单一写入者,这完全是安全的。) 除了排他性检查,借用检查器(borrow checker)还会给每个借用分配一个生命周期(lifetime),用来标记允许访问该引用的有效时间跨度。例如,一个分配在栈帧上的变量,其借用的生命周期绝不能超越对应的栈帧。如果程序员试图把一个栈上缓冲区的引用丢给另一个生命周期可能更长的线程,编译器便会直接拦截并报错拒绝编译,从源头杜绝了悬垂指针和 Use-After-Free 等内存安全隐患。 - `T`:唯一所有者,负责分配与销毁。 - `&mut T`:独占借用(可写),同一时刻全系统仅能存在一个,严禁任何其他读写引用并存。 - `&T`:共享借用(只读),同一时刻允许存在无限多个只读指针。 - 借用检查器基于生命周期验证引用的有效性,杜绝多重释放(Double Free)与释放后使用(Use-After-Free)。 ### [24:34](https://www.youtube.com/watch?v=s19G6n0UjsM&t=1474s) 互斥锁(Mutex)与读写锁(RwLock)的局限性 将 Noria 结果表的并发访问映射到 Rust 的类型系统上,就会发现两者产生了直接冲突:结果表需要支持并发的读者和写者,而 Rust 核心法则却严格禁止在有读者读取的同时进行修改。为了突破这一编译期限制,Rust 必须借助同步原语来提供所谓的“内部可变性”(interior mutability)——即通过一个共享的不可变句柄获取到底层数据的可变引用。 最基础的原语就是互斥锁 `Mutex<T>`。在 Rust 中,`Mutex` 将被保护的 `T` 包裹在内部,如果不获取锁,外部根本没有任何途径能触碰到底层的 `T`。调用 `lock()` 方法时传入的是互斥锁自身的不可变引用(`&self`),其返回的是一个具有生命周期关联的守护凭证 `MutexGuard`。只要这个守护对象存活且被独占持有,就可以通过它的内部方法拿到对底层 `T` 的唯一可变引用 `&mut T`。`Mutex` 内部凭借互斥机制确保了同一时间只有一个人能拿到这枚凭证,从而在类型系统层面安全地将共享的 `&Mutex<T>` 转化为独占的 `&mut T`。 很多人直觉上会认为只要把结果表套进一个 `Mutex` 就能万事大吉,但 Jon Gjengset 直言:“Mutexes are terrible because... well they can be okay, but in this particular instance they're really bad because they force you to have sequential access.”(互斥锁太糟糕了……它们在某些场合还行,但在我们这个场景下非常差劲,因为它们强制要求串行访问。)对于高并发的结果表,互斥锁迫使所有读和写都在时间线上排队,彻底丧失了并行处理能力。 退一步看,大家很自然会想到改用读写锁(`RwLock`)。读写锁的设计非常契合所有权规则:允许多个读者并发持有不可变借用,或者由单一写者独占可变借用。表面上看,这完美契合了读多写少的并行查询诉求;然而在高并发的数据流引擎里,只要写者需要更新,依然会阻断读者,反之读者过多也会导致写者饿死,它依然无法满足 Noria 对极致无阻断读写的严苛性能要求。 - `Mutex<T>` 机制:将数据包裹在锁内,强制通过获取 `MutexGuard` 凭证来安全产生 `&mut T`,杜绝绕过锁直接访问原始数据。 - 互斥锁瓶颈:强制全局串行化执行(Sequential Access),在 Noria 这种同时面对海量并发读与高频写的数据流场景下,吞吐量会被严重扼杀。 - 读写锁设计:支持多读单写,虽然比互斥锁提升了并发度,但写操作依然与读操作互斥,在大规模高并发吞吐场景下仍然存在明显的性能天花板。 ### [027:57] 读写锁的瓶颈与短临界区性能缺陷 在构建高并发数据库或共享状态系统时,最直接的想法是采用读写锁(`RwLock`)。但 Jon Gjengset 指出,为了验证读写锁是否真能解决并发性能问题,必须深入到底层 pthread 实现的细节中去。以 pthread 的读写锁为例,每当一个线程想要获取读锁时,其内部实际上首先必须去抢占一个排他性的互斥锁(exclusive lock);持有该锁并完成计数状态的更新后,在函数返回前迅速将其释放;在归还读句柄(release read lock)时同样要再次获取该排他锁。 ```mermaid flowchart TD accTitle: 读写锁获取读锁的内部互斥开销 accDescr: 展示读线程在获取读句柄时必须抢占内部排他互斥锁,导致短临界区场景退化为串行执行。 subgraph ReadLockAcquisition ["获取读锁过程"] A["调用 read_lock()"] --> B["获取内部排他锁 (Exclusive Lock)"] B --> C["更新读计数器状态"] C --> D["释放内部排他锁"] end subgraph CriticalSection ["临界区工作"] D --> E["短临界区:仅执行快速的哈希表查找"] end subgraph ReadLockRelease ["释放读锁过程"] E --> F["调用 read_unlock()"] F --> G["再次获取内部排他锁"] G --> H["减少读计数器状态并释放排他锁"] end ``` 这种机制导致并发效率严重依赖临界区的工作量大小:如果临界区非常长,线程拿了读锁后要做大量计算才释放,那么获取排他锁的短暂开销在整个工作量中占比极低,线程间的重叠并发度接近完美;但如果临界区极短——比如仅仅是在哈希表(`HashMap`)中查找一个键——线程刚拿到锁就立即释放,此时获取锁与释放锁本身的耗时就会占据绝大部分执行时间。换言之,读操作虽然语义上是并发的,但抢占读锁的过程却是一个串行操作,整个程序被迫重新退化成了顺序执行。 Jon Gjengset 用基准测试(Benchmark)数据印证了这一点: - **测试设置**:在 `HashMap` 上封装读写锁,X 轴为执行读操作的线程数量,Y 轴为每秒读吞吐量与写吞吐量。 - **性能走势**:当线程数量极少时,读吞吐量能够随着线程增加而上升,因为哈希查找本身存在局部的并行重叠;但随着线程数进一步增加,吞吐量曲线迅速被读锁的争用(lock contention)所主导并达到瓶颈,再也无法线性增长。 - **优化诉求**:理想情况下的吞吐量应该是一条随线程数增加而向上的直线,但单纯依赖读写锁根本无法消除排他争用。Jon Gjengset 总结指出:“all we're doing while holding the reader lock is doing a hashmap lookup. hashmap lookups are really fast, therefore we're basically benchmarking the performance of taking a lock.” --- ### [030:27] 解除锁约束与走进 Rust 中的 `unsafe` 既然读写锁因锁争用而无法实现理想扩展,就必须寻找完全无锁(lock-free)的方案。此前讨论过,锁的本质是为存在别名(aliased/shared)的指针提供安全的受控包装,赋予开发者通过共享指针修改内部数据的能力。然而,别名在无保护的情况下会直接引入数据竞争(data races),正是 C 语言中最为头疼的难题。要在 Rust 中绕过这层常规检查,就必须请出 `unsafe` 关键字。 面对台下对 `unsafe` 普遍存在的恐惧心理,Jon Gjengset 幽默地安抚道:“now in many of your heads there are going to be alarm bells going off... 'oh unsafe, I've heard about this in Rust, this is terrible, this basically gives up all guarantees and Rust is now basically C' — please breathe, it will be okay.” 他解释道,编写 `unsafe` 代码本质上就是在编写 C 语言代码。当开发者键入 `unsafe` 时,Rust 编译器只是在说“你现在被允许去做在 C 语言里能做的事情”,并没有什么额外的魔法: - **核心能力**:`unsafe` 赋予开发者的核心能力,无非就是对裸指针(raw pointer)进行解引用(dereference)。在 C 语言中解引用裸指针是理所当然的操作,但在安全 Rust 中是被严格禁止的。安全 Rust 只能解引用引用(references),因为引用带有编译期生命周期和排他性的静态契约。 - **裸指针语义**:裸指针类型如 `*mut T` 仅仅代表指向内存中某块 `T` 的地址,不包含任何生命周期信息,编译器完全不知道它是否指向栈上的失效帧,也不知道它是否被成百上千个线程同时持有。 - **契约转换**:在 `unsafe` 块中,开发者可以将一个 `*mut T` 转换为 `&mut T`(可变独占引用)。此时,开发者等于向编译器承诺:我以内部不变量保证,此刻该指针是绝对独占的,没有任何其他人正在使用它。此外,`unsafe` 也允许像 C 语言强制类型转换一样对指针类型进行转换(casting)。 - **责任归属与审计优势**:在安全 Rust 中,编译器负责保证没有数据竞争;而在 `unsafe` 中,这个责任转移到了开发者肩上。但与 C 语言整个代码库都可能存在隐患不同,Rust 的巨大优势在于故障审计的边界非常清晰:如果程序因数据竞争崩溃,开发者只需集中审查带有 `unsafe` 的代码片段,没有 `unsafe` 的地方必然严格遵守了类型系统的安全契约。 --- ### [033:15] 双缓冲指针切换与无锁并发读模型 为了在哈希表查找中完全规避锁开销,Jon Gjengset 提出了一个基于双缓冲(Double-buffering)的并发数据结构设计方案。核心架构由两个底层的哈希表(左表与右表)以及两个指针(读指针与写指针)组成。 ```mermaid flowchart LR accTitle: 双缓冲哈希表的读写指针切换流程 accDescr: 展示所有读线程通过读指针访问左表,写线程操作右表,并在适当时机原子交换指针并回写。 subgraph ReadersPool ["读线程集群"] R["多个并发读者"] --> RP["读指针 (Top Pointer)"] end subgraph Maps ["双哈希表"] MapL["左哈希表 (Left Map)"] MapR["右哈希表 (Right Map)"] end subgraph WriterThread ["写线程"] WP["写指针 (Bottom Pointer)"] --> W["单写者"] end RP -.->|"初始指向"| MapL WP -.->|"初始指向"| MapR MapR ==>|"1. 交换两指针"| MapL MapL ==>|"2. 回放写操作"| MapR ``` - **初始状态**:所有读线程都通过顶部的读指针访问左表,写线程(假设为单写者)则通过底部的写指针独立访问右表。系统始终确保读写指针指向不同的哈希表。 - **发布更新**:当写者认为时机合适、需要将此前在右表上完成的写操作公开给读者时(可以每写一次发布一次,也可以批量操作以提供最终一致性),写者会将两个指针进行原子交换(swap)。 - **指针交换后的状态**:交换之后,所有新的读操作顺着读指针被导向已经包含最新数据的右表;而写者随后会把此前施加在右表上的所有写操作重新在左表上“重放(reapply)”一遍,最终使两张表的数据完全对齐同步。 - **潜在的数据竞争陷阱**:这一天真的设想存在致命隐患。某个读者可能在指针交换前一瞬间读取了指向左表的指针,随后开始遍历左表中哈希桶的链表;此时写者完成了指针交换,自认为获得了对左表的绝对独占控制权,开始对左表进行原位修改或重放写入。此时读者与写者同时操作左表,引发数据竞争。Jon Gjengset 强调:“the reader is still looking through the buckets of that map... it's a data race and your program is gonna be wrong.” --- ### [034:47] 基于 Epoch 计数器与奇偶状态的无锁安全判定 由于整个设计的初衷就是彻底消除锁,因此绝不能通过加锁来解决上述并发冲突。系统必须在无锁的前提下,精确感知写者何时真正拥有了对旧表的“绝对独占访问权”,即确认旧表中所有的残留读者均已退出。 最初的解决思路是引入 Epoch 计数器(代际计数器): 1. **本地独立计数**:为每个读线程分配一个完全本地的计数器,这个计数器不需要在读者之间进行任何跨线程同步。 2. **读操作自增**:读者每次在哈希表上执行一次查询操作,就将自己的本地计数器加 1,该计数器本质上记录了当前读者累计完成的读取次数。 3. **写者轮询等待**:写者在交换指针后,会持续观察所有读者的 Epoch 计数器,直到看到每一个读者的计数器都向上跳动了至少一次(tick up by one)。 关于为什么观察到计数器跳动即可保证安全,Jon Gjengset 引导观众深入思考其内在逻辑:“whenever a reader ticks up its count, you know that for it to do another operation it must read the atomic pointer.” 如果某个读者的计数器发生增加,说明它在指针交换前发起的旧操作已经彻底结束,下一次发起操作时必然会重新读取已经切换后的原子指针,因此必然会转向新的右表。当写者看到所有活跃读者的计数器全部跳动了一轮,便可确信左表中已空无一人,写者即可安全独占左表。 ```mermaid flowchart TD accTitle: 基于奇偶 Epoch 计数器的读者活跃状态检测 accDescr: 读操作开始前计数加一使其变为奇数,操作完成后再加一恢复偶数,写者借此判断读者是否非活跃。 A["读者发起查找"] --> B["Epoch 计数器 +1 (变为奇数)"] B --> C["读取原子指针并遍历哈希表"] C --> D["完成查找退出临界区"] D --> E["Epoch 计数器 +1 (变为偶数)"] subgraph WriterCheck ["写者的判断依据"] F{"观察到的 Epoch 状态"} F -->|"数值是偶数 (Even)"| G["读者当前处于非活跃状态,未访问任何表"] F -->|"数值是奇数并跳动变大"| H["读者已完成上一轮读取并推进了周期"] end ``` 然而,这一机制随即面临一个严重漏洞:**非活跃读者问题(Inactive Readers)**。如果某个读者创建了句柄,但在执行了几次读取后就停止操作、永久休眠,那么写者在指针交换后就会永远陷入无限等待中,死等这个非活跃读者的计数器增加。 为了化解非活跃读者的阻塞问题,Jon Gjengset 提出了“双重自增(double increment)”结合“奇偶校验”的巧妙解法: - 读线程在正式开始读取指针并进入哈希表之前,先将 Epoch 计数器增加一次; - 在整个查找彻底完成、退出临界区之后,再次将 Epoch 计数器增加一次。 - 这意味着:读者的计数器在没有进行任何读操作的空闲期始终保持为**偶数(even)**;只有当正在进行读取时才会处于**奇数(odd)**状态。如果写者在轮询时发现某个读者的 Epoch 计数器是偶数,就可以立即断定该读者此刻根本没有在读取任何旧表的指针,处于完全的非活跃安全状态,写者无需等待其计数器继续跳动即可安全推进。 ### [37:27](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2247s) evmap 的并发不变量与微基准测试表现 Jon Gjengset 深入解释了这套无锁数据结构如何通过简单的计数器状态来保证内存安全。只要写者看到某个读者的计数器加了 1,写者就可以完全忽略该读者继续执行;此外还有一个关键约束:“如果计数器的值是偶数,你同样可以忽略该读者”。其背后的逻辑非常直观:当数值为偶数时,说明该读者当前根本没有在访问 map;即使它接下来发起访问,也必然会直接读取到写者刚发布的新指针。将这两个不变量结合起来,就能彻底达成设计目标——读者无需加锁,写者也无需加锁,所有读操作对线程完全本地化,同时还能安全地并发访问两个底层的 map。写操作和读操作均能全速运行。 ```mermaid flowchart TD accTitle: evmap 读者状态检测逻辑 accDescr: 写者检查读者局部纪元计数器的逻辑流程:若增加 1 或为偶数,写者即可安全忽略该读者。 A["写者检查读者计数器"] --> B{"计数器增加 1?"} B -- "是" --> C["安全:读者已进入新纪元,忽略该读者"] B -- "否" --> D{"计数器为偶数?"} D -- "是" --> E["安全:读者当前未在读,下一次将读取新指针"] D -- "否" --> F["阻塞等待该读者完成当前读取"] ``` 紧接着,Gjengset 展示了基准测试数据,对比了标准读写锁(RwLock)与该并发 map(即开源的 `evmap`)的性能差异。在测试中,写者在每一次写操作时都执行 map 交换,不做任何摊销优化。在相同的坐标轴下,`evmap` 的写吞吐量远高于读写锁;虽然随着并发读的增加写性能有所下滑,但这完全符合预期,因为读者变多导致写者获取轮次的频率下降。然而当把读吞吐量放在图表上时,必须大幅缩放坐标轴才能看清——读写锁的表现被衬托得极其惨淡。Gjengset 惊叹道:“你能看出不使用锁有多么重要吗?事实上,在 16 个线程下,我们做到了每秒 2500 万次读取。对于这么一个小巧的数据结构来说,这相当不错了。” - `evmap` 已经作为独立的 Rust crate 开源发布,具备完整的文档,包括所有内部机制和基准测试源码。 - 整个库的核心逻辑仅包含 7 行 `unsafe` 代码,主要用于在不变量成立(如确认写者对左侧 map 拥有独占访问权)时,安全地将裸指针(raw pointer)转换为 Rust 的可变引用(`&mut`)。 - 若想更进一步避免同时保存两份独立 map 的内存开销,通过在两个 map 之间做数据去重,只需要额外引入 7 行 `unsafe` 代码。 ### [40:19](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2419s) 复杂查询与真实生产负载(Lobsters)下的实测突破 在单操作的微基准测试之外,Gjengset 将该数据结构放入实际数据库场景中验证。针对 Web 应用中最典型的查询——例如 Reddit、Hacker News 或 Lobsters 上的帖子投票计数(`SELECT COUNT(*)`),测试团队对比了 MySQL、某商业闭源数据库(代称 System Z,受制于基准测试保密条款不能点名)、纯 Memcached 以及基于 `evmap` 构建的 Noria。在 Memcached 上,他们甚至通过多重自增(multi-incr)和多重读取(multi-get)在单次往返中完成操作,且完全不做磁盘持久化。结果显示,在 95 分位延迟指标下,MySQL 和 System Z 大约在每秒 20 万次操作时便迅速崩溃(fall over);纯内存的 Memcached 达到了每秒约 700 万次操作;而支持完整 SQL 执行的 Noria 达到了惊人的每秒 1400 万次操作。Noria 之所以能达到 Memcached 速度的两倍,正是因为读取操作完全来自无锁的 `evmap` 缓存,而 Memcached 在读取时仍需要对其数据表加锁。 为了摆脱单一查询微基准测试的局限,团队基于知名开源技术社区 Lobsters 进行了宏观端到端测试。他们获取了 Lobsters 的真实生产流量追踪(traffic traces),构建了一个真实模拟用户行为的负载生成器,按实际比例访问页面并生成全套对应查询。在 16 核服务器的压力测试下: | 系统方案 | 页面请求吞吐量(req/s) | 补充说明 | |:--- |:--- |:--- | | **MySQL(初始测试)** | ~1,100 | 最初基准测试数据 | | **MySQL(修正测试)** | 800 - 900 | 修正了查询复杂度后(原测试无意中降低了查询难度),实际吞吐显著下降 | | **Noria** | ~5,000 - 6,000 | 依赖缓存与 `evmap` 无锁结构,性能达到 MySQL 的 6 到 7 倍 | 正如 Gjengset 所强调的,这证明 Noria 绝不是玩具或理论推导,而是一个在真实负载下经过检验的高性能系统。 ### [43:23](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2603s) 学习 Rust 的心智转变:别与借用检查器对抗 总结完性能成果后,Gjengset 转向了在开发这一大型系统中学习与使用 Rust 的实际体会。他向在场工程师发出的首要警告是:“Rust 是不一样的;你不能用过去学习其他语言的方法来学 Rust。”(“Rust is different. You cannot go about learning Rust the same way you learn all these other languages you've learned in the past.”)Rust 的语法看似贴近 C 系列或 ML 语言,但这种表象具有极强的欺骗性;如果开发者只是粗浅了解语法就试图直接上手黑客式编程,必然会撞上一堵坚硬的墙——借用检查器(borrow checker)。 新手常陷入所谓的“与借用检查器搏斗”(fighting the borrow checker):坐在屏幕前被编译器疯狂报错,开发者困惑痛苦地向虚空呐喊“为什么”,根源就在于没有理解底层的所有权模型。借用检查虽然有深度,但概念本身并不深奥,关键在于系统性掌握。Gjengset 极力推荐由官方团队编写的在线书籍《The Rust Programming Language》(Rust Book),建议打破以往学习语言翻翻文档的习惯,“从头到尾把它通读一遍”,这能为日后省下海量的调试时间。对于偏好边做边学的开发者,官方的《Rust by Example》也是极佳的选择。 更关键的心态在于“顺应语言”(work with the language)。当你在 Rust 中遇到极大的阻碍,除了代码逻辑本身有 bug 之外,往往是编译器在借此向你发出设计警示:“在很多情况下,这是因为语言在试图告诉你某些事情;比如如果你卡在借用检查上,很可能是你采取的方法本身就不对。” - **典型反面案例**:在表示图(Graph)或循环数据结构时,若按照 C 语言的习惯到处乱放裸指针,在 Rust 中就会迫使你写出大量 `unsafe` 或复杂的引用计数(`Rc`/`Arc`);而符合 Rust 哲学的正确做法是将所有节点集中保存在一个列表(`Vec`)中,在图的边上只保存数组下标(index),彻底消除指针循环依赖。 - **获取社区帮助**:当怀疑编译器过于严格或自身陷入死胡同时,不要孤军奋战。OFTC/Mozilla IRC 上的 `#rust` 和 `#rust-beginners` 频道(以及 Discord 等社区)氛围极佳,在里面提问往往几分钟内就能得到包括 Rust 核心团队成员在内的专业解答。 ### [46:57](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2817s) 避免过度使用 unsafe 与掌握 Rust 的特有范式 在编写 Rust 代码时,遵循借用检查器(borrow checker)的约束具有深层的次要好处:如果编译器和借用检查器能看懂你的代码,那么其他人大概率也能理解。一旦开发者为了图省事,开始滥用 `unsafe` 标注并进行大量的指针别名(pointer aliasing)操作,虽然短期内能让程序跑起来,但埋下的隐患极大。Jon Gjengset 警告道,三到六个月之后当你重新审视这段代码时,“you will not understand why you did what you did or what invariants are being enforced by the unsafe and no one else is gonna understand it either”(你将根本无法理解自己当初为什么要这么做,或者 unsafe 究竟在维护什么不变式,其他人也同样无法理解)。因此,避免滥用 `unsafe`、不与借用检查器对着干,才是更好的实践方式。 与此同时,Jon 建议初学者务必学习 Rust 独有的语言惯用法(idioms)。Rust 吸收了许多现代语言中罕见的优秀特性,例如迭代器(iterators)。这带来了一种类似于 Haskell 的函数式编程范式;其行为也可以类比于 Python 的生成器(generators),允许开发者在任意集合上遍历,并通过链式调用写出简洁且极易阅读的代码。此外,强大的模式匹配(pattern matching)机制,以及借助问号运算符(`?`)实现优雅的错误传播和空值处理,都是 Rust 开发的核心利器。为了帮助开发者写出更地道的代码,社区开发了静态分析工具 Clippy。Jon 将 Clippy 比作 Go 语言中的 `go vet`,它就像一个不知疲倦的代码审查员,时刻提醒开发者哪些写法存在缺陷或不符合最佳实践。 - **`unsafe` 的代价**:破坏了借用检查器维护的代码自解释性与局部不变式(local invariants),长期维护成本极高。 - **语言特性亮点**:迭代器模式、强大的模式匹配、基于 `?` 运算符的结果与空值快速传播。 - **Clippy 工具**:社区出品的 Lint 工具,相当于一个自动化的 Peer Review 系统,能在开发阶段直接指出不合理的代码模式。 --- ### [48:44](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2924s) 模块化构建与编译器引导的重构 在工程组织方面,Jon 强烈提倡将项目拆分为更细粒度的 crate(Rust 的代码包/库)。在 Rust 中创建新 crate 的开销极低,添加 crate 依赖也十分轻量。这种低门槛的设计强迫开发者做好关注点分离(separation of concerns)并编写模块化代码。模块化带来的收益是多维度的:不仅提升了代码设计质量,还能显著提高编译速度,因为 Rust 支持多 crate 并行编译;同时,它也极大地改善了文档结构。Jon 特别提到可以在代码中开启 `#[deny(missing_docs)]` 编译检查,一旦开启,若有公开接口未写文档,编译器就会直接报错中断构建,从而强制维护高质量的文档。 另一个在长远开发中体现巨大的优势是 Rust 对重构(refactoring)的极佳支持。基于强大的类型系统,开发者在大规模调整架构或搬迁模块时,完全不需要战战兢兢。你可以直接大刀阔斧地重写核心接口,然后任由编译器报错并顺着报错指引一步步修复。正如 Jon 所总结的那样,“you can do compiler guided refactoring and you can basically trust the compiler”(你可以进行编译器引导的重构,并且基本上可以完全信任编译器)。对于有 Haskell 经验的开发者而言,这种“只要代码编译通过,它大概率就是对的”的感觉十分熟悉。 ```mermaid flowchart TD accTitle: 编译器引导的重构流程 accDescr: 展示在 Rust 中依靠严格类型系统和编译器报错逐步完成代码架构重构的过程 subgraph Refactor["重构阶段"] A["大范围调整类型与架构设计"] --> B["触发全量编译"] end subgraph CompilerLoop["编译器修复循环"] B --> C{"编译是否通过?"} C -- "存在类型错误 / 借用冲突" --> D["跟随编译器报错逐项修改代码"] D --> B end subgraph Result["交付阶段"] C -- "编译通过" --> E["代码结构就绪且大概率符合正确性"] end ``` - **细粒度 crate 的优势**:实现关注点分离、触发多 crate 并行编译以加快构建速度、促使模块拥有独立的良好文档。 - **`#[deny(missing_docs)]`**:语言内置的 Lint 属性,缺少文档即直接拒绝编译。 - **类型驱动的重构**:完全信赖编译器的报错指引,极大地降低了大型系统架构演进的心智负担。 --- ### [50:14](https://www.youtube.com/watch?v=s19G6n0UjsM&t=3014s) 统一的生态系统与对并发并发症的顿悟 Rust 的包管理与文档生态展现出了极高的一致性。得益于官方统一的代码格式化工具(`rustfmt`)和自动生成的文档系统(docs.rs),所有开源库的排版风格高度一致,各 crate 之间的文档超链接能自动关联跳转,使得阅读和集成第三方依赖极其顺畅。尽管当时的生态相比老牌语言仍显年轻,部分特定领域的库可能缺失,但这也为开发者提供了自行开源并贡献生态的绝佳机会。 在演讲尾声,Jon 引用了一段深有共鸣的长文,以此总结 Rust 对并发认知带来的根本性重塑。该段引言指出,Rust 最引以为傲的核心卖点是消除数据竞争(data races),编译器在编译期就会从物理上杜绝多个线程在未同步的情况下同时修改同一份数据。这种强硬的机制带给程序员两重认知:第一阶段是对编译器及其借用检查器的折磨感到怨恨,直到把代码调通;第二阶段则是突然意识到,自己过去在其他语言中写出了多么庞大数量的、充满数据竞争的不安全代码。 Jon 结合自身的经历感慨,过去写的并发代码能跑通,往往只是因为侥幸或者底层库碰巧加了防御措施,并非自己的水平有多高;正如引文所说,“my lack of failure is more attributable to random chance than skill”(我过去没踩坑,更多是归咎于随机的运气,而非真凭实学)。在彻底习惯了 Rust 之后,他发现自己成了一名更优秀的程序员。即便回头再去写 Python 或 C 语言,只要看到某些并发模式,大脑就会立刻警觉并意识到“Rust 编译器绝对不会接受这段代码,所以它必定是有 bug 的”。最后,Jon 感谢了现场的观众与主办方 Larry,并开放现场问答与线下的邮件、GitHub、Twitter 交流通道。 - **生态一致性**:统一的工具链保证了所有第三方库的代码风格一致,docs.rs 实现了依赖树内部文档的无缝互联。 - **Rust 对思维的改造**:借用检查器的约束倒逼开发者掌握正确的并发思维,消除对“凭运气运行”的代码盲目自信。 - **演讲结语**:Jon 总结了本次涵盖 Noria 底层细节与 Rust 高层语言特性的分享,在全场掌声中结束了演讲。 ## Notable quotes > [43:23](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2603s) Rust is different. You cannot go about learning Rust the same way you learn all these other languages you've learned in the past. — Jon Gjengset > [48:44](https://www.youtube.com/watch?v=s19G6n0UjsM&t=2924s) you can do compiler guided refactoring and you can basically trust the compiler — Jon Gjengset ## Mentioned Two Sigma, Larry Rudolph, Trevor, MIT CSAIL, Jon Gjengset, PDOS, Noria, Rust, Mozilla, Haskell, OCaml, Go, C, C++, Python, JavaScript, Firefox, Cargo, crates.io, docs.rs, Intel DPDK, AWS EC2, GTK, Qt, Redis, Memcached, MySQL, Facebook, Naiad, Apache Spark, evmap, Lobsters, Reddit, Hacker News, Clippy, rustfmt ## Video description This is a guest lecture I gave at Two Sigma in November 2018 where I discussed the experience of using Rust for building larger, high-performance systems. In it, I cover what makes Rust an attractive option for such projects; Noria, the high-performance research database prototype I've built using Rust; an interesting concurrent data-structure we use in Noria; and how I've found Rust to work in that context. The presentation slides are available at https://jon.thesquareplanet.com/slides/rust-twosigma/ . You can read more about Noria at https://pdos.csail.mit.edu/noria . [Source](https://www.youtube.com/watch?v=s19G6n0UjsM)

Source quotes

[43:23] Jon Gjengset: Rust is different. You cannot go about learning Rust the same way you learn all these other languages you've learned in the past. [48:44] Jon Gjengset: you can do compiler guided refactoring and you can basically trust the compiler

Source quotes

[10:58] Jon Gjengset: If you just want to write something and not worry about the details, Rust is an awful language for that job because Rust will force you to write correct code. [7:11] Jon Gjengset: In Rust, lots of concurrency bugs are impossible—the compiler will not let you compile a program that has a data race in it [30:27] Jon Gjengset: Whenever you type the unsafe keyword, Rust is saying you're now allowed to do the things you're allowed to do in C, there's nothing more or less magical to it. [43:23] Jon Gjengset: The first thing I want to tell you is Rust is different. You cannot go about learning Rust the same way you learn all these other languages you've learned in the past.

Original transcript

[0:00] hi welcome everybody I'm Larry Rudolph [0:02] from the labs team part of the goal the [0:05] Lapps team is to reach out to academia [0:07] and this is a special talk I met John at [0:14] csail when we were when Trevor and I [0:17] were there talking about security of [0:19] firmware and we had this idea that we [0:22] should use rust and so we met so there's [0:25] the MIT rest expert or the Northeast [0:27] rustic spirit or the worldwide rust [0:29] expert I don't know which one call [0:31] yourself with but his research is also [0:34] on databases novel ways databases and [0:39] coordination and conflicts and stuff [0:43] that I really like [0:44] so when inviting to give a talk I said [0:45] you have to talk about both things and [0:47] John said no problem [0:50] I guess he comes here from I guess you [0:53] had undergrad in Australia and then you [0:56] went to London and I was from Norway you [1:00] know he went the wrong way and then [1:02] finally sort of came back up and ended [1:04] up at MIT and he's a grad student at my [1:06] Tec cell but let's so he's going to give [1:09] a talk and then there's be a deep dive [1:11] or extended Q&A if you want to stay and [1:13] ask but let's make him welcome thank you [1:21] so I'm John thanks for the introduction [1:24] I'm here to talk to you about rust in [1:27] general and in particular about a [1:28] research system that I've been building [1:29] at MIT for the past three years it is [1:32] essentially a fast concurrent database [1:34] that tries to get a database is slightly [1:36] differently from how we've been [1:37] designing them in the past and I part of [1:40] what I want to impart is what it's like [1:41] to use rust to build something that's [1:43] sort of large and and real in some sense [1:45] because I think that's a lot of the [1:47] reservations that people have about rust [1:49] so I'm John I'm a PhD student at the MIT [1:52] parallel and distributed operating [1:54] systems group I work on a database [1:55] called nuria that's now about sixty [1:57] thousand lines of russ code that we've [1:58] been developing over three years I also [2:00] maintain several open source rust [2:02] libraries I work a lot on the [2:04] asynchronous i/o part of rust I've done [2:07] about 30 hours or so of live rust coding [2:09] stream so you can find online if you're [2:11] interested and so if you want to look [2:13] any of that then you can find me online [2:14] at any of those URLs or just shoot Larry [2:17] and email he will forward you to me I'm [2:20] sure he will do that correctly as for [2:23] this talk there are some things we're [2:24] gonna cover and some things we're not [2:25] gonna cover and I think it's useful to [2:27] just set that up in advance in [2:28] particular I'm going to look at rust [2:30] from a relatively high level I'm not [2:32] gonna dive into like the syntax and code [2:34] I am gonna talk a little bit about rust [2:37] ownership because it's one of the more [2:39] novel parts of rust one of the things [2:41] that a lot of newcomers to rust struggle [2:43] with but I will not sort of go deep into [2:46] any of that we can do do that in the [2:47] deep dive afterwards I'll also give the [2:50] overall architecture of nuria this new [2:51] database and in particular dive into one [2:55] of the interesting data structures we [2:56] have to design for nuria that hopefully [2:58] will appeal to the engineers in the room [3:00] which is I assume basically all of you I [3:02] will not talk a lot about sort of query [3:05] execution query planning deep database [3:08] stuff if you want to read any more about [3:10] that you should look at the paper or [3:11] come talk to me after in general the [3:14] idea of this talk is to look at higher [3:15] level aspects of using Russ for real and [3:18] so therefore the first thing we're gonna [3:19] look at is what is rust like why am I [3:22] talking to you about rust at all well [3:24] rust is a programming language by [3:26] Mozilla it's relatively new it reached [3:28] 1.0 at about four years ago and it's for [3:31] systems programming which is a very [3:33] vague term but the basic idea behind [3:35] systems programming is you want to have [3:39] a language that lets you build systems [3:41] that do stuff so they're not necessarily [3:44] just sort of tools or command-line [3:46] utilities but they're for writing like [3:48] servers or clients or basically anything [3:51] as a system so it's very simply poorly [3:54] defined but the RUS team likes to use a [3:56] bunch of these sort of quotes that [3:58] define what the language does one of the [4:00] ones they're relatively well-known for [4:02] is this fast reliable productive pick [4:04] three and the idea is that in a lot of [4:06] existing production languages yours are [4:08] forced into this I have to choose away [4:10] something I have to choose to not get a [4:12] particular feature like in C you don't [4:14] get save code but you get really high [4:16] performance and Russ wants to get a do [4:18] away with those trade-offs instead [4:20] saying let's try to do all of them in [4:22] particular Russ takes this approach with [4:24] concurrency so in rust they have the [4:26] slogan of fear [4:27] concurrency you should be able to write [4:29] code that is concurrent without worrying [4:31] that you have data races than your code [4:32] is gonna crash all the time the Russ [4:34] language is also very community driven [4:36] so it is entirely open source everything [4:39] is run on github there's an RFC process [4:41] for extended the language for sending [4:42] the compiler and I have several things [4:45] that are now in the compiler available [4:47] in the language and you can - and you [4:49] can get involved in that process and [4:50] that is one thing that is really neat [4:52] about this language as opposed to many [4:53] other languages Russ is on a six-week [4:56] release cycles and new things also get [4:58] released fairly quickly now you might [5:02] wonder well how is referred a lot of [5:04] there are other programming languages [5:06] out there like why are we not all using [5:07] Haskell or oh camel or go for that [5:10] matter what is different about rust well [5:12] the first thing is rust is a really [5:14] powerful type system you can think of [5:16] this in terms of sort of more like [5:17] Haskell than it is like C this turns out [5:21] to let you express constraints in your [5:23] code a lot better than you can do if you [5:25] don't have good types you get zero cost [5:27] abstractions so the idea is that you can [5:29] have abstraction layers you can have [5:31] things like interfaces but the code want [5:33] amorphous is so that the code is still [5:34] fast you get the same kind of speed as C [5:37] you have the borrow checker which tries [5:39] to guarantee that you have no data races [5:41] at runtime so the compiler will check [5:45] that you have no data bases in your code [5:46] and the tooling of Russell is just [5:48] really really excellent it hasn't [5:51] actually taken the time to learn from [5:53] the past sort of n decades of [5:55] programming language development and [5:56] infrastructure development and [5:58] incorporates that into the language so [6:01] what do you what would you use for us [6:03] for well Russ is really good for writing [6:05] low level systems code so the sort of [6:06] space is traditionally occupied by C or [6:08] C++ and part of the reason for this is [6:11] because as opposed to many other [6:12] languages it explicitly tries to be [6:14] interoperable with C and C++ that means [6:17] that if you have existing libraries you [6:19] have in existing systems that are built [6:21] using those languages you can take rust [6:23] and you can swap out just small parts of [6:25] it and still have it work well with the [6:27] existing software that you have and in [6:28] fact this is why Mozilla developers in [6:30] the first place it was sort of the [6:31] firefox browser to be able to swap out [6:34] components of it with something that was [6:35] highly concurrent and written in Russell [6:37] they knew that it wouldn't crash [6:38] but you can also write high-level [6:40] software in rust so rust because of its [6:42] type system is quite pleasant to work [6:44] with and you will find that you can [6:46] actually write code that looks almost [6:48] like Python code except that it is also [6:50] fast and also like the compiler will [6:53] check if you have an incorrect type as [6:55] opposed to you know crash at runtime in [6:57] fact I've found that I write both high [6:59] level systems code sort of network [7:01] services servers like I wouldn't go and [7:03] I can write tools and infrastructure [7:04] like command line utilities and rust and [7:06] feel perfectly happy doing so but let's [7:11] dive into that's like why specifically [7:12] I've talked in very vague terms let's [7:14] look at some specifics so first of all [7:16] and this is an important point in rust [7:18] lots of concurrency bugs are impossible [7:21] the compiler will not let you compile a [7:23] program that has a data race in it and [7:25] if you come from basically any other [7:28] language this is very surprising right [7:30] in C you will write a program and you [7:32] expect it to crash at runtime the first [7:34] few times do you run it this is not the [7:35] case with rusts code it can crash at [7:37] runtime for sure but there are lots of [7:39] these problems that just will not be [7:41] there you also no longer have things [7:43] like use after free or double free the [7:45] entire ownership system guarantees is [7:48] also at compile time the type system [7:50] provides things like enums and you might [7:52] think oh I know about enums like we have [7:54] younam's in other languages in rust [7:55] enums or what known as abstract [7:58] algebraic data types and algebraic data [8:01] types are basically enums that can [8:02] contain other stuff in particular for [8:06] error handling rust uses the result enum [8:08] the result enum is generic over two [8:10] parameters the value that is contained [8:12] if you get an okay and the value that is [8:14] contained if you get an error so if you [8:16] think of writing go code for example you [8:18] often return this sort of couple of the [8:21] value if everything went correctly and [8:22] an error and you need to know like if [8:25] error is nil then you do something [8:27] otherwise do something else or you might [8:28] like return a null pointer or something [8:30] like that if something went wrong in [8:31] rust you just return a result that you [8:34] the compiler forces you to check is [8:37] either okay and then it will give the [8:38] value inside that okay where it's an [8:40] error and it will give you the value of [8:41] that error so this means that it forces [8:43] you to explicitly deal with things like [8:44] errors null pointers as well are [8:46] encapsulated in enums is an option type [8:48] that is either none or its [8:50] that contains the value that it is [8:52] actually pointing to all of these also [8:54] come at zero costs at runtime and [8:55] finally rust has this thing called [8:57] pattern matching which is survey you can [8:59] think of it as the switch statement but [9:00] on steroids unless you do things like if [9:03] this is let's say this computation [9:07] succeeded and also the value it gave was [9:10] between 4 and 17 and also this other [9:13] value is the string foo you can do [9:16] really advanced pattern matching which [9:18] helps write sort of code that feels more [9:20] Python s core even Java Script e then it [9:22] feels like low-level C code and I talked [9:25] about the tooling a little bit so Russ [9:27] says dependency management built-in so [9:29] you can think of things like NPM for [9:32] JavaScript or pi PI for python or even [9:36] for go things like go get except it's [9:38] much more advanced you can do [9:40] dependencies that are path dependencies [9:43] that are gift dependencies that are [9:44] dependencies on the central library [9:47] infrastructure that rust has and all of [9:49] it is just built into the default [9:50] tooling that you get and works really [9:51] well the documentation is built into the [9:54] language for every library every file [9:56] you write if you take a function a [9:58] module even a constant you can prefix it [10:01] with a comment with three slashes [10:03] instead of two now that is documentation [10:05] if you write code in that string it will [10:07] be automatically turned into a test so [10:09] the your documentation cannot be out of [10:11] date in any code samples that it has all [10:13] of this is integrated in local language [10:15] any library that you upload to the [10:17] repository of libraries that rust has [10:19] all of that code is automatically [10:21] documented and interlinked so that any [10:23] documentation you look up will just [10:25] always look the same it will always be [10:27] interact with other crates is actually [10:29] fabulous to use and I do not use that [10:32] term lightly and finally and I mentioned [10:36] this a little bit when it comes to C and [10:37] C++ it is really easy to do F of I so [10:40] foreign function calls whether that is [10:42] calling into a c library is C++ library [10:44] there's interoperability with Python [10:46] with Ruby with go that you can make [10:49] calls into those languages or of those [10:51] languages call interest very very easily [10:53] and that means that it's easy to [10:55] integrate into existing code base [10:58] now all that said there are some things [11:00] that Russ is not good for and I think [11:03] it's important to highlight those in [11:04] part because the language is improving [11:06] in these regards but also because it's [11:08] worthwhile to know the first of these is [11:09] that Russ is not yet great for highly [11:11] specialized systems and by that I mean [11:13] if you have something like Intel DPD K [11:16] or very highly vendored libraries that [11:19] are large and expansive and written in [11:22] some other language it is not trivial to [11:25] just sort of start using from Rosland [11:27] you couldn't take DP DK and generate [11:29] sort of bindings and rust for all of the [11:31] seed libraries they give you but it [11:33] would be a lot of work and if no one [11:35] else has done that work yet that's a lot [11:37] of overhead you have to do and that you [11:38] might not want to maintain that is [11:40] getting there slowly but surely the [11:42] people are developing really good [11:44] infrastructure for DP DK or for Amazon [11:47] ec2 or for whatever other sort of large [11:49] project that exists out there today but [11:52] it is slowly getting there the other [11:55] thing is Russ is not great for rapid [11:57] prototyping if you're just kind of like [12:00] do a quick and dirty write something up [12:02] just you're just gonna run at one so you [12:03] don't really care if it crashes if you [12:05] just want to write something and not [12:07] worry about the details Russ is an awful [12:09] language for that job because Russ will [12:11] force you to write correct code not [12:13] always it will not always be correct but [12:15] it is pretty unrelenting about if you're [12:18] if you're gonna write code and it's [12:19] gonna compile it shall not have [12:21] databases like you are not allowed and [12:23] the type system is fairly involved like [12:25] it forces you to deal with errors and [12:26] force you to do with the case when [12:28] things are null and that's sort of [12:31] contrary to the idea of rapid [12:32] prototyping linear kernel programming [12:35] also a little bit finicky with Russ [12:37] currently because you don't have things [12:38] like Lib C the standard library it is [12:41] possible some people have done it but [12:43] writing kernel modules and Russ still [12:45] requires a bit more work GUI programming [12:47] because gree programming is hard and [12:49] they're there bindings to gtk and QT and [12:52] win32 even for Mac OS there are a bunch [12:55] of bindings for the the UI libraries [12:57] there but the bindings are still pretty [12:59] young in part because the language is [13:00] young too an asynchronous networking is [13:04] one thing that Russ is doing a lot of [13:05] working currently on building good [13:07] primitives for dealing with asynchronous [13:09] computation in general and i/o in [13:11] particular the language is taking a [13:14] pretty interesting approach that looks a [13:15] little bit like promises in JavaScript [13:17] and it works pretty well I've been using [13:20] that in noria in the database I've been [13:22] building at its highly performant but [13:24] the code can get a little bit nasty to [13:26] write because not all the high-level [13:27] distractions are in place yet and so [13:29] that is also some where the language is [13:31] still developing okay so let's get to [13:35] the database part of this talk [13:37] we've talked a little bit about what [13:39] rust is and I'm gonna switch sort of [13:41] sites entirely and talk about databases [13:43] for a second but hopefully something [13:44] that you find interesting [13:45] so nori has this project I've been [13:47] working on for a while and the basic [13:49] observation is that you have a bunch of [13:51] applications that issue queries like [13:52] this this is extremely common that you [13:55] just have your application issue lots of [13:57] sequel queries in this case this is it [14:00] as a story's table and a votes table you [14:02] count up the votes for every story and [14:03] you want to extract the story title and [14:05] the vote count for some specific story [14:07] all well and good what this ends up [14:10] being in a traditional relational [14:12] database is it turns into something like [14:14] this you have a front-end that issues [14:15] that query to the back-end database the [14:17] database does some query planning and [14:19] decides that oh in order to execute this [14:21] I have to count the things in boats and [14:23] then I have to join it with stories and [14:24] I'm gonna do a filter and let me give [14:26] the results back this is all very well [14:28] and good this is what basically every [14:29] database out there today does the [14:32] problem is for most applications almost [14:35] all of the stuff that you do is read [14:38] almost all your operations are selects [14:40] and the selects in a database are pretty [14:42] expensive because every time you do a [14:44] read you have to redo this computation [14:46] so even if the data that's underlying [14:49] you hasn't changed you have to do all [14:51] this stuff on every read and if 90% of [14:54] your requests are reads that is a lot of [14:56] wasted cycles and we don't like to waste [14:58] cycles so some of you might think ah we [15:00] solve this we're just gonna put a cache [15:02] there right and caching solves [15:04] everything like we're just gonna stick [15:06] memcache key or Redis or something in [15:08] front of our database and who everything [15:10] is fast but unfortunately it's not quite [15:13] that simple in fact your application is [15:15] more likely to look like this where you [15:17] have to make sure that like every time [15:19] you write you invalidate the appropriate [15:20] you don't eval aday to much because then [15:22] you throw your entire cache away and now [15:24] when you do a read you might miss you [15:25] have to go to the database you can run [15:27] into really weird situations that many [15:29] people don't even think about like if [15:30] you have a skewed distribution of [15:32] popularity in your database then it [15:34] might see a lot of rights and a lot of [15:36] reads you get a right for a popular key [15:38] you invalidated all of the reads now [15:41] start missing because they were all for [15:43] that popular key all of them go to your [15:45] database at the same time in your [15:46] database falls over and this is known as [15:48] thundering herd and it requires a lot of [15:50] really sophisticated mitigation to get [15:53] around there's a facebook paper on how [15:55] Facebook's tries to manage their my [15:57] sequel and memcache D caches and runs [16:01] into this problem and the solutions are [16:02] highly non-trivial all of this seems [16:05] like so much complexity in order to make [16:07] your application fast so what we did in [16:09] Oriya was essentially observed that all [16:12] of this is stupid the database has your [16:15] queries so why can't the database just [16:17] maintain the cache wouldn't that be a [16:19] lot easier so nuria instead of doing all [16:23] of that business does it a regular sort [16:25] of query planning like you do in [16:26] traditional databases but instead of [16:28] executing the query on read it execute [16:30] the query on write and it just so [16:32] incrementally so every time a new every [16:34] time a new write comes in its gonna look [16:36] at all the queries the application has [16:38] given it in the past and it's gonna [16:40] basically take the existing query [16:43] results for that query take the new [16:45] write process it through the query plan [16:47] and update the cache results in place [16:50] this may seem a little strange so let's [16:53] do a quick walkthrough of that so here's [16:55] the same query that we looked at earlier [16:56] but notice that there's this results [16:58] thing at the bottom which is that what's [17:00] known as a materialized view you can [17:02] think of it as your cache it stores the [17:04] current results for that query and you [17:08] see that the currently for some key a [17:10] the story title is foo in the vote count [17:12] is 41 now a new vote comes in for story [17:16] a it enters the this is a data flow [17:20] graph that basically represents the [17:21] query that we're executing the vote for [17:24] a comes into the count operator the [17:26] count operator goes oh the previous [17:27] count was 41 the new count is 42 so it [17:30] emits a record that says a is now 42 [17:32] instead of where it was [17:33] 41 that record then hits the join [17:37] operator the joint operator goes oh I [17:39] got a thing for a I'm gonna look it up [17:40] his stories and find the title so it [17:42] time for words a is 42 and the title is [17:44] fou that then enters the cash and the [17:47] cash goes oh I have a key for a already [17:49] I'm gonna replace it with the new value [17:51] for a so now is 42 so now any at this [17:55] point any read that comes in is gonna [17:57] read the old vote count and at this [17:59] point any query for that view or for [18:02] that query that comes in is gonna see 42 [18:04] is about count and so the application [18:06] all it has to do is do a single lookup [18:08] into the cache it never has to do any [18:10] invalidations the right just go to the [18:12] database and the database does the right [18:13] thing your cache is always up-to-date so [18:16] that's the basic idea behind Nuria and [18:19] it turns out this works really really [18:21] well I'll show you some benchmarks later [18:24] this is a great idea but it does have [18:27] some challenges like this is research [18:29] after all the first of these challenges [18:31] is you can't just store all the results [18:33] for all the queries you've ever executed [18:35] that is not okay right you don't have [18:37] enough memory for that at least not yet [18:39] and so you need some mechanism to ensure [18:42] that you can evict things from your [18:44] cache but that is not straight forward [18:46] into this design imagine that we threw [18:48] of it away that all of the results for [18:50] the story a and then a new vote comes in [18:53] for the story a the count operator no [18:56] longer knows what the previous count was [18:58] so it can't emit what the new vote count [19:00] is and so you need a mechanism your [19:02] system to ensure that even if you throw [19:04] something away or something gets asked [19:06] for that you've never been asked for [19:07] before that you have some way of [19:09] computing it or recomputing it this is [19:12] the problem of partial materialization [19:15] that we will not get into in this talk [19:17] but it is a really interesting problem [19:19] the second one is that I've just shown [19:22] you a single query and I've shown you a [19:24] crate that does not change over time but [19:26] in reality applications don't just issue [19:28] one query they issue many queries and [19:31] beyond that over time the application [19:32] develops the application grows and [19:34] evolves and it might change existing [19:36] queries or add new ones and the database [19:39] has to react to that the database has to [19:41] know what to do and it is not trivial [19:43] because existing data flow systems so [19:45] you might have heard of things [19:46] like Nyad or for that matter even spark [19:49] does not really deal with things [19:51] changing while they're running you sort [19:53] of have to throw the thing away and [19:54] start up a new instance this has the new [19:56] set of queries we don't want to do that [19:58] because that's a lot of extra work we [20:00] all the system to just dynamically adapt [20:02] and that is something that Nuria is able [20:03] to do and you will have to read the [20:05] paper to figure out why the third [20:08] challenge I'm gonna tell you about and [20:09] this is the one we're gonna dive into is [20:10] you'll notice that in noria we have this [20:13] results table that is where all of your [20:16] rights go there's basically your cash [20:17] but it's also where all the reads go now [20:20] you have lots of readers and lots of [20:22] writers all trying to hit the same thing [20:24] right that is problematic you can't just [20:27] do that unless you have some kind of [20:28] synchronization because a writer might [20:31] update a record while it's being written [20:32] so we need some kind of synchronization [20:34] here and we want that to be really fast [20:36] because they're gonna be lots of reads [20:38] and lots of writes to this at the same [20:40] time so we're gonna dive into challenge [20:43] three four one and two you should be the [20:45] paper I can send out the the length of [20:47] the paper afterwards the I promise you [20:49] the solutions to the other problems are [20:50] also very interesting but I do not have [20:52] the time to cover them today but we will [20:54] talk about that last challenge because [20:56] it happens to introduce and require a [20:58] pretty interesting data structure before [21:01] we do that though we're gonna have to [21:02] talk a little bit about some basic [21:04] computer science in particular we're [21:06] gonna talk about this notion of [21:07] ownership so ownership is a key tenant [21:10] of rust and the basic idea is similar to [21:12] what REI is and C++ or in many other [21:16] languages that if you own something then [21:19] you are responsible for that thing so if [21:21] you own some piece of memory you are the [21:23] one who's responsible for freeing that [21:25] memory whenever it's appropriate to do [21:26] so in particular rust extends this width [21:30] if you own something you also get to [21:32] choose who has access to that resource [21:35] and how in rust there are basically [21:38] three types you could have for some type [21:40] cheese so think of T here is any type in [21:43] your system it does not really matter it [21:44] could be a string it could be a number [21:46] could be an array whatever for any key [21:48] you can either have just the T which [21:50] means that you own that thing only it [21:52] means that you are responsible for [21:54] freeing it [21:55] you can have a mutable reference to tea [21:57] that's how to read that second part [21:58] immutable reference to tea is just like [22:00] a pointer to tea in any other language [22:02] but with the additional contract or the [22:04] additional guarantee that no one else is [22:07] able to read or write that tea as long [22:10] as you have that mutable reference you [22:12] have exclusive access to that tea but [22:15] you do not have any responsibility to [22:17] free it after you're just borrowing it [22:19] the last type is an immutable reference [22:22] to tea it is also just a pointer to tea [22:24] but with the additional contract that [22:25] you are not allowed to modify tea [22:27] because there are going to be other [22:29] people who are also reading tea these [22:32] are the basic three types of tea and [22:33] this is basically the entire ownership [22:35] system in rust that these are the three [22:37] types and rust will at compile time [22:40] check that you have not violated these [22:42] contracts it's gonna check at compile [22:44] time that for every variable for every [22:47] piece of memory for every resource in [22:49] your program you never have both [22:52] immutable reference and an immutable [22:53] reference at the same time do you never [22:55] have multiple mutable references either [22:59] of those not okay your program will not [23:01] compile it is fine though to have [23:03] multiple immutable references right [23:05] because you can have lots of things read [23:06] things at the same time and you can [23:08] think of this always almost as the [23:09] compiler requires you to prove that your [23:11] program does not do this and it turns [23:14] out that's all you need to guarantee the [23:16] program has no data races right if you [23:19] never have two things if you never have [23:21] something modify a thing while it's [23:24] being read or modified you cannot have [23:26] data races because you will either just [23:29] have multiple readers or you'll have a [23:30] single writer which is fine [23:32] you also guaranteed that you only ever [23:34] free things once because the owner is [23:37] responsible for freeing it and you can [23:38] only have one owner [23:39] furthermore the the borrow check are [23:42] also checked you don't use anything [23:44] after you have gotten rid of it in [23:47] particular this is how the borrowing [23:48] system works it adds this notion of a [23:50] lifetime so if you borrow any tea that [23:54] borrow of tea is assigned a lifetime and [23:56] you can think of the lifetime as how [23:58] long you're allowed to access tea for so [24:00] for example if he lives on the stack the [24:02] lifetime that's given out for any borrow [24:04] of tea it's gonna be tied to the stack [24:06] frame of the thing that [24:08] as T and the compiler will check that [24:10] when T goes away when that stack frame [24:12] is popped and T is freed there are no [24:15] outstanding references to T so if you [24:18] try to take say a reference to something [24:20] sort of the stack like a buffer or [24:22] something and give it to another thread [24:23] the compiler would go no that thread can [24:26] live longer than this stack frame [24:27] therefore your program is not safe so it [24:29] will not compile so why am I telling you [24:34] all this this seems like a lot of sort [24:35] of CS nonsense well it turns out you can [24:37] frame the problem that we looked at in [24:39] the beginning of this concurrent access [24:41] to the results table as ownership right [24:44] because what we're trying to do is allow [24:46] concurrent reads and writes to a shared [24:49] structure which russ has just told us is [24:52] not okay you are not allowed to have [24:54] something mutate in something read at [24:56] the same time the waste way rust mist [24:58] mitigates this is by forcing us to use [25:01] synchronization so there's certain [25:02] primitives in rust that allow you to [25:05] take an immutable handle and get a [25:07] mutable handle to a tea one of these you [25:10] have probably heard of its called a [25:11] mutex right in rust you will not be [25:14] allowed to access a shared value without [25:16] using something like a mutex does it [25:18] specifically have to be a mutex it has [25:20] to be something that safely provides a [25:23] mutable access to something that is [25:25] shared so let's look at what a mutex [25:28] looks like in rust in in rust you have a [25:31] mutex in this case that contains a tea [25:33] again tea can be any type and notice [25:36] that in rust the mutex wraps the tea so [25:38] you have no way of getting at the tea [25:39] without also taking the lock when you [25:42] lock a mutex it takes a immutable [25:45] reference to self here I've annotated [25:47] annotated it with a lifetime of MTX for [25:49] mutex right so the the thing you get [25:52] back from the ejects can't outlive the [25:53] mutex because the mutex would have gone [25:54] away and what you get back is this [25:56] single the mutex guard which you can [25:58] think of as a sort of credential that [26:01] you are currently holding the lock right [26:03] so when you take the lock you take an [26:04] immutable reference to the mutex and you [26:07] get back a handle to the inside of the [26:09] lock that inside of the lock has a get [26:12] method that as long as you have Mew if [26:14] as long as you have exclusive access to [26:17] the credential you get exclusive [26:19] to the tea right so what this entire [26:23] chain is saying is that as long as the [26:26] mutex is still alive and you have the [26:28] lock you can get a mutable reference to [26:31] whatever is inside the lock and what [26:33] this is giving us is mutable access to a [26:35] tea through a shared reference to the [26:38] mutex and it does this by mutual [26:40] exclusion right the internally the mutex [26:44] knows that as long as it doesn't look [26:45] keeping right this is safe to do because [26:48] no one else is gonna have a mutable [26:49] reference to that tea because they [26:51] wouldn't have the lock they wouldn't [26:52] have the credential therefore they can't [26:54] access the team a micro well isn't this [26:57] problem to solve like you just wrap our [26:59] wrap our results table in a mutex and [27:01] everything's good well no quite so [27:03] mutexes are terrible because well they [27:06] can be ok but in this particular [27:08] instance they're really bad because they [27:09] force you to have sequential access if [27:11] you think of our results table you [27:13] either have to do a read or a write for [27:15] any given unit time which is terrible [27:18] because we want to do things in parallel [27:19] we want this to be current and fast and [27:21] new Texas are neither concurrent nor [27:22] fast so you might go hah but I know I [27:25] know what you're gonna say use an RW [27:28] mutex yeah so reader/writer lock is [27:31] something that you may or may not be [27:32] familiar with the basic idea behind [27:34] behind a reader/writer lock is that you [27:36] can have multiple readers take a lock at [27:39] the same time or you can have a single [27:41] writer take the mutex but you cannot [27:43] have both this has a similar flavor to [27:46] ownership right you can either have a [27:47] single mutable point or exclusive [27:49] pointer or you can have many immutable [27:51] and that's great [27:52] right it lets us do many reads in [27:54] parallel awesome that's all we wanted [27:56] and then every now and again you sort of [27:57] have to take a break and do some writes [27:58] is that good enough do we think that [28:00] this solves our problem so in order to [28:03] answer that question we need to look a [28:05] little bit at how reader/writer locks [28:06] work so this might be a little small [28:09] terrine but the exact details are not [28:12] important just trust me that this is [28:13] actually how reader/writer locks work [28:14] and pthreads the basic thing to observe [28:17] is that there's a lock in here so [28:21] whenever you take a reader the reader [28:23] part of a lock so the thing that's [28:25] supposed to let many threads take the [28:27] reader lock at the same time whenever [28:29] you do that you have to [28:31] take this exclusive lock first you don't [28:33] take him for very long right notice that [28:34] you release it before you return but you [28:37] have to take it whenever you take the [28:39] read handle and whenever you give it [28:40] back so what this means is that it is [28:46] okay if you have a long critical section [28:48] because taking that lock is very little [28:51] of your workload right if you're taking [28:55] the read lock and then doing lots of [28:56] work and then releasing the read lock [28:58] taking the read lock is probably not [28:59] where you're spending your time and so [29:01] the overlap is gonna be almost perfect [29:02] you'll get lots of concurrency however [29:04] if you have a short critical section [29:06] then you're gonna take the reader lock [29:08] you're gonna immediately release it [29:10] because you're only did a few [29:11] instructions in between if you're doing [29:14] that [29:14] taking the reader lock is going to be [29:15] your bottleneck but taking the reader [29:17] lock is a sequential operation and so [29:20] now you're basically ending up with your [29:22] program being sequential again all we're [29:25] doing while holding the reader lock is [29:27] doing a hashmap lookup how's my glue [29:29] lookups are really fast therefore we're [29:31] basically benchmarking the performance [29:33] of taking a lock and I'm going to prove [29:37] that to you by running a benchmark so [29:40] this is a reader/writer lock on top of a [29:43] hashmap and what we're doing along the [29:45] x-axis is that we are increasing the [29:46] number of threads that are doing reads [29:48] of the map and I'm showing on the y-axis [29:50] the number of reads per second and the [29:52] number of writes per second and you can [29:54] see that initially it's fine right like [29:55] we get more threads and we get more read [29:57] throughput that's great but over time [29:59] we're getting dominated by taking this [30:02] lock in the read so you see with few [30:04] threads we do get the speed-up [30:06] because there's some parts where the [30:08] hashmap gets get to happen in parallel [30:09] but when you get to more threads you're [30:12] spending all your time contending on the [30:13] lock that's not good the question is can [30:17] we do better right we'd like this to be [30:18] a straight line it would be the best [30:20] thing we can possibly hope for you can't [30:22] really have it be super linear that [30:23] would be weird so now we need to do with [30:27] that locks so remember how locks provide [30:29] a safe wrapper around aliased or shared [30:33] pointers right this is what we talked [30:34] about earlier it gives you mutable [30:35] access through a shared pointer but [30:38] aliasing brings data races right this is [30:40] the whole problem that we haven't see [30:41] that you might just have two things [30:43] the point of the same thing and try to [30:45] mutate it so we're gonna have to need [30:47] unsafe now in many of your heads so [30:50] we're going to be like alarm bells going [30:51] off now oh and say if I've heard about [30:53] this in wrestling this is terrible this [30:54] basically gives up all guarantees and [30:56] Russ is now basically see please breathe [30:59] it will be okay so it turns out what did [31:02] I do [31:04] no again there you go it turns out that [31:09] writing unsafe code is really just [31:11] writing C code whenever you type the [31:13] unsafe key word Russ is saying you're [31:15] now allowed to do the things you're [31:16] allowed to do and see there's nothing [31:19] more or less magical to it in fact all [31:21] of the basically everything that unsafe [31:23] lets you do is against you the ability [31:25] to dereference a role pointer nothing [31:28] more nothing less [31:29] which from Sealand seems obvious like of [31:31] course I can be referenced wrong [31:32] pointers in rust you cannot it is not [31:35] okay to dereference or pointer you can [31:37] dereference references because they have [31:39] a certain contract associated with them [31:40] but a row pointer dereferencing it who [31:43] knows how many other people have that [31:45] same row pointer it is not okay but [31:47] unsafe lets you do it in particular we [31:50] can introduce this concept of a row [31:52] pointer so a star mute of T is just a [31:55] pointer to T with no additional contract [31:57] it says nothing about who else might [31:59] have that pointer all that's saying is [32:00] that I know the memory location of a T [32:02] it has no lifetime associated with it [32:04] which inherently is unsafe right who [32:06] knows this could point into the middle [32:08] of the stack somewhere and that function [32:09] might have returned who knows and then [32:12] unsafe let's you take one of these and [32:14] turn it into a mutable reference to T so [32:18] basically you can give it out along with [32:19] the contract saying I promised that this [32:23] pointer is now exclusive to you no one [32:25] else is using it and doing that is [32:28] unsafe because you and are internally [32:30] maintaining some invariant that makes [32:31] that true you can also do things like [32:35] changing types using this right you can [32:36] this is basically pointer typecasting [32:39] and see [32:42] the idea of unsafe is that you are now [32:45] responsible for guaranteeing that there [32:46] are no data races because there's no [32:48] associated contact with the Pointer [32:50] anymore you are claiming by writing [32:52] unsafe that you know that this is okay [32:55] which is what you're doing all the time [32:56] in C it's just in rust you have to be [32:58] explicit about when you do that and [33:00] what's nice about this is that now you [33:02] know exactly which parts of your code to [33:04] audit if you crash with the data race [33:06] because it's all the places that have [33:08] unsafe a place that does not have unsafe [33:10] is forced to follow the contract so [33:15] we're going to use them safe and we're [33:17] going to come up with a following plan [33:18] so this is the data structure that we're [33:19] gonna end up implementing we're gonna [33:22] keep two maps the we're gonna keep two [33:25] pointers one pointer is going to point [33:27] to one map the other pointer is going to [33:29] point to the other map all of the [33:30] readers are going to go through the top [33:32] pointer and all of the writers in this [33:35] case let's just imagine there's just one [33:36] it's gonna go through the bottom pointer [33:38] and we're gonna make sure they always [33:40] point to different maps right so all the [33:43] readers initially go to the Left map all [33:45] the rights initially go to the right map [33:48] whenever the writer decides that they [33:51] want to reveal the rights that they've [33:52] done to the readers this could be every [33:54] right or they could decide to do it less [33:56] often so to give eventual consistency [33:57] but whenever they decide that the rights [33:59] should be revealed they're gonna swap [34:02] both pointers right so the rights are [34:04] then gonna go to the left and the reads [34:05] are now going to go to the right and [34:07] notice that the reads now get to observe [34:09] the new rights and then the writer is [34:12] gonna reapply the rights that it has [34:15] done to the right map to the left map [34:16] right so that now the two maps are again [34:19] up to date but there's a problem with [34:21] this if you just did this naively there [34:23] could still be readers left in the left [34:26] map imagine that there's some reader who [34:28] read the pointer and then it's gonna [34:31] like start to look through the buckets [34:32] of the hash map but then the writers [34:35] swap the pointer so now the writer [34:37] thinks that they're allowed to modify [34:39] the map but the reader is still looking [34:41] through the buckets of that map but it's [34:43] not okay it's a data race and your [34:44] program is gonna be wrong so we need [34:47] some mechanism to deal with this right [34:49] now [34:51] the way to deal with this [34:53] is not to add locks because we're not [34:55] allowed to do that right locks would [34:57] trivially solve this problem but locks [34:59] are the problem in the first place we're [35:00] not gonna have any locks we need to know [35:02] when the writer has exclusive access to [35:04] the left map when are there no more [35:06] readers left the way we're going to do [35:09] this is by introducing epic counters so [35:11] every reader is going to keep just a [35:13] local counter there knocking it's not [35:15] going to be synchronized in any way this [35:16] is going to keep a local counter to that [35:18] reader and every time they do an [35:21] operation on the map they're going to [35:23] increment that counter so they're [35:25] basically like a counter of how many [35:27] reads I've done and then what we're [35:30] gonna do is we're gonna have the writer [35:32] look at all the epic counters it's gonna [35:35] do the pointer swap look at all the epic [35:37] counters until it is seen all of them [35:39] tick up by one I want you to think about [35:44] that for a second why is that okay why [35:45] is that sufficient why is that invariant [35:47] enough to guarantee that there are no [35:48] readers well think of it this way [35:50] whenever a reader takes up its count you [35:53] know that for a to do another operation [35:55] it must read the atomic pointer so if it [35:59] does or if it's in the middle of some [36:01] read then who knows whether it's read [36:04] the pointer or not but if you see it [36:06] increase you know that it is finished [36:08] whatever operation it did in the past [36:09] and it is now about to read the atomic [36:12] pointer again so if you observe a tick [36:15] in every reader after you swap the [36:16] pointer you know that all of the readers [36:19] must now have seen the new value [36:21] therefore they must all now be in the [36:23] right map and therefore the writer has [36:25] exclusive access to the left side now [36:28] this is not sufficient either now some [36:31] of you may be able to realize why in [36:35] particular what if a reader is inactive [36:37] what if you have a reader that just [36:39] never does any reads they're just gonna [36:41] sit there like they created a handle to [36:43] the map and maybe they did some reads in [36:46] the past but now they've stopped doing [36:47] reads the writer is going to just keep [36:51] waiting keep waiting keep waiting and [36:53] see the counters increase for all the [36:56] readers but whichever ones are inactive [36:58] this seems problematic what do we do [37:01] about this [37:02] so we're gonna pull another trick we're [37:05] gonna say that you're gonna increment [37:07] your epic counter twice [37:08] you're already incremented once before [37:11] you start reading and again after you've [37:13] finished so what this means is that if [37:18] you ever observe a counter to be even [37:20] then you know that that reader is not [37:22] reading the pointer you know that that [37:24] reader is inactive so again we still [37:27] have the guarantee that if you see it [37:28] increased by one you're all good you can [37:30] ignore that reader we have the [37:31] additional constraint that if you ever [37:33] see it being even you can also ignore [37:35] that reader and the way to think about [37:36] that is if that value is even it means [37:39] that that reader is not currently [37:41] looking at the map which means that if [37:43] it were to look at the map it will see [37:45] the new pointer therefore seeing it even [37:47] is all we need so these two invariants [37:51] combined gives us the guarantees that we [37:53] wanted namely that the readers are [37:56] taking no locks the writers are taking [37:58] no locks all the read operations are [38:00] entirely local to that thread and we [38:03] still have safe access to both Maps [38:05] right so writes can proceed at full [38:07] steam reads can proceed at full steam so [38:11] this is pretty good right remember that [38:14] this was the graph for that we had for [38:16] the reader/writer lock now we're gonna [38:19] now do is look at the exact same [38:21] experiment they're using this other map [38:23] and we're gonna have the right swap the [38:26] maps on every right operation so there's [38:28] no amortize me or anything is just gonna [38:30] swap on every single operation what we [38:32] get is this this is on the same axis and [38:36] you see that the rights are like a lot [38:38] faster than the ones for you to read a [38:40] lock they are still going down but [38:42] that's sort of what we expect because [38:43] there are more rights happening sorry [38:45] there are more reads happening and so [38:47] the writer gets to take their turn less [38:49] often so that's as expected but it is [38:51] doing a lot better but where are the [38:52] reads well we have to zoom out a little [38:56] bit so this is the reader/writer lock on [38:59] a slightly different scale and you see [39:01] that it's really not that good [39:03] like reader/writer locks are terrible [39:05] here's how we do can you see how much it [39:10] matters to not use locks right [39:12] in fact this is with 16 threads we get [39:15] to do 25 million reads per second that's [39:19] pretty good that's pretty good for a [39:21] little data structure so this is [39:23] actually a crate that you can use in [39:25] your own projects this is a library that [39:27] is put out there it's fully documented [39:28] all of the internals are documented as [39:30] well the benchmarks are open sourced [39:32] there are seven lines of unsafe code in [39:34] this entire library and the places where [39:38] it's unsafe is basically to ensure that [39:41] whenever we know that an invariant is [39:44] true like for example we know that we [39:46] have exclusive access to the left map as [39:47] a writer then we have then we're willing [39:49] to turn that raw pointer that we keep [39:52] into a rust mutable reference because we [39:55] know by by virtue of our invariants that [39:58] this is now saved so that's pretty neat [40:02] several lines of unsafe there are seven [40:03] more lines of unsafe that avoids keeping [40:05] both Maps you still keep two Maps but [40:08] you deduplicate the data between the two [40:11] maps but then you need another seven [40:13] lines of unsafe but I feel like that [40:15] might be okay so let's go back to the [40:19] sequel I've given you sort of a micro [40:22] benchmark here of how this performed [40:23] with just that basic operation and [40:25] compared to redirected locks but what [40:27] happens at bigger scales so let's just [40:29] look at this query for a second this is [40:30] a relatively common query that you see a [40:32] lot in web applications right you're [40:33] just like counting the number of votes [40:35] for a thing [40:36] it happens in reddit and happens in [40:37] hacker news it happens in lobsters it [40:39] happens in all sorts of website to do [40:40] this so what we did was we took this [40:43] exact query and we ran it on my sequel [40:46] we ran it on System z which is a [40:49] database system I can't name because [40:51] they have a benchmarking clause we ran [40:54] it against pure memcache key [40:56] so just memcache key obviously doesn't [40:59] execute sequel but in memcache key we [41:01] just do multi increments so we just use [41:04] the inker operator in memcache key and [41:06] multi gets so it's single round-trip for [41:09] all the operations and of course that's [41:10] sort of not really even a realistic [41:13] benchmark cuz it's not persisting [41:14] anything but let's just go with that and [41:17] we also ran it on Nuria and Nuria uses [41:19] this evie map this map that we just [41:21] talked about the results are like this [41:25] so the x-axis here is the number of [41:27] requests per second we give the system [41:28] and the y-axis is to the 95th percentile [41:30] latency do we observe four requests [41:32] notice that my sequel and system see [41:34] fall over almost immediately I think the [41:37] number there is somewhere around two [41:38] hundred thousand operations per second [41:40] memcache T gets to about seven million [41:43] operations per second and we get to [41:45] fourteen the difference between us and [41:48] memcache T is surprising here right [41:50] because we're actually doing full sequel [41:52] execution but it's because our reads are [41:55] just reading from cache and our cash [41:57] just uses this data structure so there's [41:59] no locking memcache she on the other [42:01] hand does do locking on reads it locks [42:04] the table set of uses and that's why [42:05] we're about twice as fast as memcache T [42:07] so it turns out that this kind of data [42:09] structure optimization actually really [42:11] helps so let's look at something that's [42:13] not as micro as this this is still [42:15] pretty micro single query so we did was [42:17] we took the lobsters website which is [42:19] sort of like hacker news the difference [42:22] is that it is open source and we were [42:24] able to get production traffic traces [42:26] from it we took those and we built a [42:30] workload generator that basically [42:32] emulates user requests so it just loads [42:35] different pages with the same kind of [42:37] distribution as you see in the real work [42:38] load and then we issue the same queries [42:42] as loading that page would have issued [42:44] what we see is this with my sequel you [42:48] get around like 1,100 page requests per [42:52] second you can serve with nor you get [42:54] about 5,000 in the latest version of the [42:57] code my sequel actually gets worse [42:59] because it turned out that we were [43:01] giving it an easier query than we [43:03] thought [43:03] so my sequel is really around like 800 [43:06] or 900 this is not thousands right 800 [43:09] 900 page requests per second was on a 16 [43:12] core server whereas Nuria gets to about [43:15] 6,000 so this is a pretty significant [43:18] speed-up that we see both from caching [43:20] and from this additional data structure [43:23] so I've taught you a lot about sort of [43:25] data structures in performance now so [43:26] let's go a little bit back to what this [43:28] experience has been like this has all [43:29] been to show you that this is a real [43:31] system like this is not a joke it's a [43:33] real system that is very fast and on [43:35] real workloads so now let me try to tell [43:38] you a little bit about what it's been [43:39] like both to learn rust and use rust [43:41] with this system so the first thing I [43:44] want to tell you is rust is different [43:45] you cannot go about learning rust the [43:48] same way you learn all these other [43:50] languages you've learned in the past it [43:53] is true that its syntax is sort of [43:55] relatively C or ml like like it's not [43:57] that bad just to look at it but that's a [44:00] little bit deceptive it is not just [44:02] another C or C++ and go if you just try [44:05] to get the gist of it and just like hack [44:07] your own code you will hit a wall and [44:09] that wall is the borrow checker and you [44:12] might have heard this phrase of fighting [44:13] the borrow checker that is what you're [44:15] gonna end up doing you're gonna be [44:17] spending a lot of time sitting there or [44:18] the compiler yells that you're going [44:20] you're not allowed to do that and you go [44:21] why why and then you're just gonna be [44:25] screaming into the void because you [44:26] don't understand the model that's [44:27] underlying it I've tried to give you [44:29] some inkling of what it's like and it's [44:31] not actually all that complicated but [44:33] you do need to learn the model so the [44:36] learning curve is basically learning [44:37] borrow checking now there's a lot of [44:40] depths to that but even just a cursory [44:42] understanding is gonna get you pretty [44:43] far I really recommend that you read the [44:46] rust book so the Ross programming [44:48] language book is an online book written [44:49] by the rust team it is very good and you [44:52] should read it start to finish this is [44:55] not something we normally do when [44:56] learning a language but with rust that [44:58] is actually worthwhile doing it will [45:00] save you so much headache and so much [45:01] time there's also something called rust [45:04] by example for those of you who sort of [45:06] prefer learning by reading code rust by [45:08] example goes through many of the same [45:09] concepts as the rust book does but it [45:11] also by giving you code examples every [45:13] step of the way and you can compile and [45:15] run the code samples too to try to [45:17] figure out and sort of understand how [45:19] tweaking the code helps these two tiny [45:21] URLs at the bottom are links to [45:23] resources for busy outlines for how you [45:27] should go about learning rust they may [45:28] or may not be useful to you but I [45:31] recommend you check them out if you're [45:32] interested [45:33] the other thing is you really want to [45:35] work with the language and this is not [45:37] just apply to rust but I think it [45:39] applies especially to rust if something [45:42] feels hard if you get stuck on something [45:44] it could just be because your program is [45:46] wrong that is true [45:47] but in many cases is because the [45:49] language is sort of trying to tell you [45:50] something like if you get stuck on Baro [45:52] checking is probably because the [45:54] approach you're taking might not be the [45:55] right one [45:56] so one example of this is if you end up [45:58] with cyclic data structures so you want [46:01] to represent say a graph for example and [46:04] you try to represent it just by having [46:05] pointers everywhere like you wouldn't [46:06] see you couldn't totally do that you [46:09] might have to either do one say for [46:11] reference count me or something the [46:13] alternative is store all the nodes in a [46:15] list and then store the indices in the [46:17] graph and now you have no cycles right [46:20] so you're sort of fighting the borrowed [46:22] tracker because you're trying to [46:23] represent something in a in a weird way [46:25] like you could use reference counting [46:27] but you should you should really sort of [46:29] second-guess your own design if the [46:30] compiler is really fighting you on [46:32] something in some cases the compilers [46:34] just being stupid so there's a RC [46:37] channel there also discord channels and [46:39] guitar channels and all the other things [46:41] but I'm pretty good the IRC channel this [46:43] is called rust on Mouse net is really [46:45] good there's also a rust beginners [46:47] channel there if you get stuck like ask [46:49] a question and someone probably from the [46:51] rust core team will come back to you in [46:53] like five minutes it's fantastic there's [46:57] also a secondary argument to this which [46:59] is if the compiler if borrow checker [47:01] understands your code the it's a higher [47:03] chance that someone else would [47:04] understand it too if you start adding [47:05] lots of unsafe annotations it's just [47:07] like do lots of pointer type aliasing [47:09] you can do that but chances are like six [47:13] months later or three months later when [47:14] you come back and look at this code you [47:16] will not understand why you did what you [47:17] did or what invariants are being [47:19] enforced by the unsafe and no one else [47:22] is gonna understand it either so if you [47:24] can avoid unsafe and not have to fight [47:26] the bar checker that's probably better I [47:29] also recommend that you learn the isms [47:31] of the language rust has some some [47:33] things that other languages current [47:35] languages don't really have so [47:38] iterators is a good example of this so [47:40] sort of ends up being a slightly [47:41] functional pattern so those of you [47:43] experience with Haskell might be more [47:44] familiar [47:45] them iterators are basically you can [47:47] walk any collection any you think of it [47:50] as a generator and Python and you can do [47:52] really neat things just based on [47:54] iterators that make your code a lot [47:56] easier to read and write pattern [47:58] matching is fantastic error propagation [48:00] and rust and null pointer propagation [48:03] and rust is really easy using the [48:04] question mark operator which will become [48:06] intimately familiar with if you start [48:08] writing Russ code and they're just gonna [48:10] save you a lot of time to learn them I [48:12] wrote a blog post a while ago when I [48:15] discovered many of these myself that you [48:17] may find interesting if you start diving [48:19] into rust there's also a really good [48:21] tool built by the community called [48:23] Clippy so Clippy is a tool that will [48:25] take your rust code and tell you all the [48:28] things you probably shouldn't be doing [48:30] it's like basically someone wrote a [48:32] thing that's gonna do peer review for [48:34] you and it's gonna yell at you for like [48:36] don't do that don't do that don't do [48:37] that it's great it's like go vet for [48:40] those who used to go i also recommend [48:44] that you build crates so crate and [48:46] russell is basically a library but [48:48] they're really really easy to make new [48:50] ones the overhead to making any one is [48:52] really low the overhead for adding a [48:55] dependency of one is really low and the [48:56] reason is because it basically forces [48:58] you to do separation of concern it [49:00] forces you to write modular code it also [49:02] gives you faster builds because rust [49:05] compiles multiple crates in parallel and [49:06] so it speeds up your build process and [49:09] finally it gives you much better [49:10] documentation and incentivizes you to [49:12] write documentation for each module [49:14] separately and rust has amongst other [49:18] things are linked that you can turn on [49:19] in the compiler itself in the language [49:21] that says deny missing docs you set that [49:24] up to top your library and now the come [49:26] your program will not compile unless [49:28] everything is documented it's fantastic [49:32] the other thing that's really neat and [49:34] that that I've observed over time in [49:36] rust is that refactoring is really [49:40] straightforward and this is in part [49:41] because of the type system right you can [49:44] decide to move a bunch of stuff around [49:45] and then you've let the compiler guide [49:47] you to all the changes you need to make [49:49] and when your program finally compiles [49:51] chances are it's basically right right [49:53] because your types work out and that [49:54] means that the code is all now in the [49:56] right place [49:57] you can do compiler guided refactoring [49:59] and you can basically trust the compiler [50:02] this is for those of you program in [50:04] Haskell in the past is a similar kind of [50:06] feeling of when it compiles it's [50:08] probably pretty close to correct all [50:10] right there's still gonna be bugs like [50:11] trust me it'll still be bugs I also [50:14] recommend you take advantage of the [50:15] ecosystem because the dependency [50:18] management is so good it is really easy [50:20] to add dependencies on projects that [50:21] other people are written there are a lot [50:23] of really good libraries out there that [50:25] you can just add a dependency on and it [50:27] works the code formatting tools are good [50:30] enough in rust that you can look at [50:32] other people's crates and basically [50:34] understand what they're like [50:35] the formatting is consistent the [50:37] documentation is consistent and because [50:39] it's auto-generated for all published [50:40] libraries and interlinked automatically [50:42] it is really easy to just look up a [50:45] crate you're gonna see its documentation [50:46] you see how it fits together with yours [50:48] and if you upload your library will be [50:50] automatically interlinked with that [50:52] crate that you depend on as well the [50:55] ecosystem is still a little bit young so [50:56] there might be some libraries you don't [50:57] find but then maybe you can write that [50:59] library I'm gonna leave you with this [51:02] quote that I think is really good it's a [51:04] little bit long I'm gonna read it out [51:07] loud but you can also read it internally [51:08] one of the value propositions most [51:10] frequently lauded by Russ developers is [51:12] its freedom from data races the compiler [51:15] will literally not allow you to build [51:16] code that could ever produce a situation [51:18] where two threads can mutate the same [51:20] data this teaches us two things loathing [51:23] of the rust compiler and it's borrow [51:24] checker until we make things work and to [51:27] the extent to which we've written unsafe [51:29] race run code in other languages I've [51:31] learned a great deal from that second [51:33] bullet point I look back at some of the [51:35] code I've written in the past and I [51:36] realized the only reason it worked is [51:39] because some other library had [51:40] guardrails in place or the lack of data [51:43] resis was an accident in other words my [51:45] like a failure is more attributable to [51:47] random chance than skill I really feel [51:50] like this when writing rusts code now [51:52] that it has made me a better programmer [51:54] I feel like I now see data races in my [51:57] code and I realize that the compiler is [51:59] about to yell at me if I go back now and [52:02] write Python code or C code I noticed [52:05] that there are data races were because [52:08] I think all the Ruskin pod would never [52:09] accept this program so therefore it must [52:11] be wrong if I look at old code that I [52:14] have written in other languages I [52:15] realize that that code is probably [52:18] broken I wish I had the russ compiler to [52:21] tell me what was wrong with it [52:22] right I I've tried to cover both sort of [52:28] low-level details about Nuria and [52:31] high-level details about Russ in general [52:32] if you have any questions at all about [52:34] any of the stuff I talked about today [52:35] then feel free to follow up me either by [52:37] email that's my email or I'm on github [52:41] and Twitter and all the other things [52:42] under the username on the right feel [52:44] free to reach out to me or through Larry [52:45] if you want and I'm happy to take any [52:47] questions you might have [52:48] thank you [52:49] [Applause]