惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Privacy & Cybersecurity Law Blog
WordPress大学
WordPress大学
Last Week in AI
Last Week in AI
腾讯CDC
人人都是产品经理
人人都是产品经理
小众软件
小众软件
V
Visual Studio Blog
S
Secure Thoughts
J
Java Code Geeks
V
V2EX
量子位
The Hacker News
The Hacker News
酷 壳 – CoolShell
酷 壳 – CoolShell
Security Latest
Security Latest
博客园_首页
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Spread Privacy
Spread Privacy
博客园 - 叶小钗
T
Threat Research - Cisco Blogs
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Tailwind CSS Blog
Cloudbric
Cloudbric
S
SegmentFault 最新的问题
AI
AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Application and Cybersecurity Blog
Application and Cybersecurity Blog
IT之家
IT之家
T
Tenable Blog
S
Security @ Cisco Blogs
月光博客
月光博客
雷峰网
雷峰网
博客园 - 【当耐特】
Know Your Adversary
Know Your Adversary
C
Cybersecurity and Infrastructure Security Agency CISA
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Hugging Face - Blog
Hugging Face - Blog
爱范儿
爱范儿
Attack and Defense Labs
Attack and Defense Labs
博客园 - 三生石上(FineUI控件)
Hacker News - Newest:
Hacker News - Newest: "LLM"
有赞技术团队
有赞技术团队
N
News and Events Feed by Topic
阮一峰的网络日志
阮一峰的网络日志
TaoSecurity Blog
TaoSecurity Blog
宝玉的分享
宝玉的分享
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
The Cloudflare Blog
K
Kaspersky official blog

Arpit Bhayani

Temporal Primer - Building Long-Running Systems What Matters in Production RAG Structure of Every LLM Chat How LLMs Really Work Your Monolith Is Already A Distributed System Databases Were Not Designed For This BM25 JOIN Algorithms Venting at Work Comes at a Reputation Cost Why Half Your Skills Expire Every Few Years Multi-Paxos - Consensus in Distributed Databases MySQL Replication Internals Bloom Filters When You Increase Kafka Partitions Product Quantization The Q, K, V Matrices The Day I Accidentally Deleted Production How LLM Inference Works What are Blocking Queues and Why We Need Them Heartbeats in Distributed Systems How Writes Work in Apache Cassandra Redis Replication Internals How to Handle Arrogant Colleagues at Work How Does a CDN Handle Content Replication You Can't Fix Everything on Day One When Emotions Spill Over at Work Why gRPC Uses HTTP2 Meetings With No Agenda Are a Waste of Time Career Longevity Beats Constant Job Hopping Stay Relevant at Higher Salary Levels Why Distributed Systems Need Consensus Algorithms Like Raft Why Do Databases Deadlock and How Do They Resolve It Why and How Cache Locality Can Make Your Code Faster Why Eventual Consistency is Preferred in Distributed Systems Why does DNS use both UDP and TCP Should You Do a Master's My Honest Take Empathy Makes Great Engineers Unstoppable Good Mentors Build People, Not Just Skills Why You Should Always Have Back-Burner Projects Before You Push Back, Know What You're Standing On Be the One They Can Count On How Much Are People Willing to Bet on You How to Get Leadership to Say Yes to Your Project Don't Let Your Best Ideas Die in Silence Be the Person Everyone Wants to Work With The XY Problem and How to Avoid It The Startup Hiring Lie Nobody Talks About You Won't Be Promoted Unless You Ask It's Not Enough to be Right; Learn to be Heard No One Ships Great Software Alone You Don't Win by Proving Others Wrong Appreciate Generously; It Costs Nothing, But Builds Everything Your Soft Skills Aren't Soft at All Before you form an opinion, experience it Why You Need Both Curiosity and Action to Thrive A Daily Worklog Changed Everything How We Handle Mistakes Defines Us Own Your Mistakes Don't Wait. Step Up. Temporary Fixes Are Permanent Why Interviews Are Biased And What Sets You Apart Saying 'This isn't my problem' is actually the problem How to Write Effective OKRs Never Lose a Battle due to Miscommunication When In Doubt, Code It Out How to Follow Up Without Annoying People Lead Projects That Land, Execution Over Everything Abstract Thinking Will Define Your Next Decade We Engineers Suck at Task Estimation Shiny Obect Syndrome in Tech When to Change Jobs - The 3P Framework Comfort and Competition - Know When to Switch Gears Paper Notes - On-demand Container Loading in AWS Lambda Paper Notes - SQL Has Problems. We Can Fix Them Pipe Syntax In SQL Paper Notes - NanoLog - A Nanosecond Scale Logging System Don't Wait, Learn - The Best Resource is Mythical Paper Notes - WTF - The Who to Follow Service at Twitter The Unexpected Benefit of Reading Random Engineering Articles Roadmaps Are Limiting Your Growth Stop Leaving Money on the Table - Negotiate Your Job Offer Never Bad-Mouth Your Past Employers Show You're a Culture Fit Quantify your resume, Know Your Numbers The Importance of Being Likeable in Interviews Questions to Ask Your Interviewer How to Build Trust Through Collaboration Do This, Once You Are Out of the Interview Cycle Stop Pitching Ideas, Start Pitching Projects Read Those Design Docs, Even the Ones That Seem Irrelevant The Best Engineering Lessons Happen During Outages Great Engineers Start Broad LLM Summaries are Ruining Your Learning Turn System Design Interviews into Discussions Title Inflation At Work, Find Your Own Projects 6 Simple Strategies to Cracking Any Tech Interview How to Remain Unblocked Solving the Knapsack Problem with Evolutionary Algorithms Generating Pseudorandom Numbers with LFSR Local vs Global Indexes in Partitioned Databases
Fully Persistent Arrays
Arpit Bhayani · 2021-02-15 · via Arpit Bhayani

Persistent Data Structures preserve previous versions of themselves allowing us to revisit and audit any historical version. While we already did an exhaustive introduction in the previous essay, in this essay, we take a detailed look into how we can implement the Fully Persistent variant of the most common data structure - Arrays.

The essay is loosely based on the paper titled Fully Persistent Arrays for Efficient Incremental Updates and Voluminous Reads by Tyng-Ruey Chuang and we implement it using Backer’s trick elaborated in the paper A Persistent Union-Find Data Structure.

Fully Persistent Arrays

An array is an abstract data structure consisting of n slots to hold a maximum of n elements such that each element is identified by at least one index. A typical array allows the following functions - create(n), update(index, value) and get(index).

The simplest form of an array, that we all are familiar with, is the Linear Array that is designed to hold elements in consecutive memory locations, leveraging spatial locality for faster and more efficient retrievals and scans. Before we jump into the implementation details of Fully Persistent Arrays, let’s reiterate what exactly are Fully Persistent Data Structures.

Persistent Data Structures preserve previous versions of themselves allowing us to revisit and audit any historical version. Fully Persistent Data Structures allows access and modification to all the historical versions as well. It does not restrict any modifications whatsoever. This means we can typically revisit any historical version of the data structure, modify it like we are forking out a new branch.

https://user-images.githubusercontent.com/4745789/107117958-e112dd80-68a3-11eb-971b-58034e693f44.png

Fully Persistent Arrays are arrays that support Full Persistence which means it supports usual array operations while also allowing us to go back in time and make updates to any of the previous versions. We define the following operations on Fully Persistent Arrays -

  • create(n) - returns an array of size n having all the slots uninitialized
  • update(array, index, value) - returns a new array identical to array except for the element at the position index. The parent array array remains unaffected and is still accessible.
  • get(array, index) - returns the element present at the index index in array array

Implementing Fully Persistent Array

A naive way of implementing these arrays is to do a Copy-on-Write and keep track of historical versions. This approach very inefficient as it requires m times the memory required to hold n elements, where m is the total number of versions of the array.

https://user-images.githubusercontent.com/4745789/107803148-3cebd380-6d88-11eb-9889-bb551e83c00a.png

A better way of implementing these arrays is by using the Backer’s Trick which enables the required functionality with just one array and a tree of modifications.

Fully Persistent Arrays using Backer’s Trick

A more efficient way of implementing Fully Persistent Arrays is by using a single instance of an in-memory Array and in conjunction use a tree of modifications. Instead of storing all the versions separately, Backer’s trick allows us to compute any version of the array by replaying all the changes asked for.

Tree of modifications

The tree of modifications is an n-ary tree that holds all the versions of the array by storing only the modifications made to the elements. Each version is derived from a parent version and the root points to the in-memory cache array which holds the initial version of it.

https://user-images.githubusercontent.com/4745789/107856766-71c35d80-6e50-11eb-9f59-c3744cdc884f.png

Each node of the tree holds three fields - index, value, and a pointer to the parent, making this tree pointing upwards towards to root. Thus each node holds the changed value, where did the change happen index and on which version the change happened parent.

Say we changed the element at the index 1 of the array 9, 6, 3, 5, 1 to 7 we get array 9, 7, 3, 5, 1. The tree of modifications has 2 nodes one root node a0 pointing to the initial array, and another node a1 denoting the updated version.

https://user-images.githubusercontent.com/4745789/107858298-996af380-6e59-11eb-99dc-7a68ea25f5b4.png

The node a1 has 3 fields, index set to 1, value set to 7 and parent pointing to a0. The node implies that it was derived from a0 by changing the value of the element at the index 1 to 7. If we try to branch off a0 with another change say index 4 set to value 9 we would have 3 nodes in the tree. Thus we see how an update translates into just creating a new node and adding it at the right place in the tree.

Now we see with this design how we implement the three functions of an array create, update, and get.

Implementing create

The create function allocates a linear array of size n to hold n elements. This is a usual array allocation. While doing this we also create the root node of our tree of modifications. The root node, as established earlier, points to the cache array.

# The function creates a new persistent array of size `n`
def create(n: int) -> Array:
    # 1. allocate in-memory cache
    # 2. initialize the tree of modifications
    # 3. make the root of the tree point to the cache
    pass

The overall complexity of this operation is O(n) space and O(n) time.

Implementing update

The update operation takes in the index that needs to be updated, the value, and the version of the array on which update is to be made.

# The function updates the element at the index `index` with value
# `value` on array `array` and returns the newly updated array
# keeping the old one accessible.
def update(array: Array, index: int, value: object) -> Array:
    # 1. create a node in the tree and store index, the value in it
    # 2. point this new node to the parent array
    pass

To do this efficiently, we create a new node in the tree whose parent is set to the array version on which update is performed, index and value are set what was passed during invocation. Thus we see that the update operation takes a constant O(1) space and O(1) time to create and represent a new version of the array.

With the update operation being made efficient we have to trade-off get operation.

Implementing get

The get operation takes in the index that needs to be fetched and the version of the array from which the element is to be fetched. The get operation seeks no extra space but takes time proportional to the distance between the array version and the root. In the worst case, this distance will be as long as the total number of versions of the array.

# The function fetches the element from index `index`
# from the array `array` and returns it.
def get(array: Array, index: int) -> object:
    # 1. Start from the requested array and traverse to the root node.
    # 2. Allocate a new register to store the requested value
    # 3. During traversal, if the node.index == `index` update the
    # register with the value.
    # 4. return the value of the register
    pass

The overall complexity of this operation is O(1) space and O(n) time.

Optimizing successive reads on the same version

We established that the update operation takes constant time and reads are expensive. If our system is write-heavy, then this is pretty handy but if the system has more reads then operation taking O(n) time hampers the overall performance of the system. So as to optimize this use case we take a look at the operation called Rerooting.

Rerooting

The initial array (the first version of the array) has no significance to be the root forever. We can reroot the entire tree such that any child node could become the root and the value it points to - cache - represents the true copy of the array. Rerooting is a sequence of rotations to make the desired array version the root.

The algorithm for rerooting is a classic Backtracking algorithm that requires updates in all the nodes coming in the path from the old node to the new node.

https://user-images.githubusercontent.com/4745789/107935855-ee1c8480-6fa7-11eb-9870-2ae90d6460a9.png

The rerooting operation takes time proportional to the distance between the old and new root ~ O(n). Since the successive reads are happening on the same version the get operation becomes O(1) as well. Thus depending on the kind of usage of the system we can add rerooting step in either get or update operation.

References