Rust 内存操作与 GapBuffer 实现
1. RefWithFlag 与 Nullable Pointers
在 Rust 中,
RefWithFlag
是一个不安全类型,Rust 虽不清楚其内部具体情况,但会尽力提供帮助。若省略
behaves_like
字段,Rust 会提示参数
'a
和
T
未使用,并建议使用
PhantomData
。
RefWithFlag
采用与
Ascii
类型类似的策略,避免在不安全代码块中出现未定义行为。该类型本身是公开的,但字段并非公开,这意味着只有
ref_with_flag
模块内的代码才能创建或查看
RefWithFlag
值。
对于 Nullable Pointers,Rust 中的空原始指针与 C 和 C++ 一样,地址为零。对于任意类型
T
,
std::ptr::null<T>()
返回
*const T
类型的空指针,
std::ptr::null_mut<T>()
返回
*mut T
类型的空指针。检查原始指针是否为空有几种方法,最简单的是
is_null
方法,而
as_ref
方法可能更方便,它接受
*const T
指针并返回
Option<&'a T>
,将空指针转换为
None
。同样,
as_mut
方法将
*mut T
指针转换为
Option<&'a mut T>
值。
2. 类型大小与对齐
任何
Sized
类型的值在内存中占用固定字节数,且必须位于某个对齐值的倍数地址处,该对齐值由机器架构决定。例如,
(i32, i32)
元组占用 8 字节,大多数处理器希望它位于 4 的倍数地址处。
可以使用
std::mem::size_of::<T>()
获取类型
T
的值的字节大小,使用
std::mem::align_of::<T>()
获取其所需的对齐值。例如:
assert_eq!(std::mem::size_of::<i64>(), 8);
assert_eq!(std::mem::align_of::<(i32, i32)>(), 4);
任何类型的对齐值始终是 2 的幂。类型的大小总是向上取整为其对齐值的倍数,即使技术上可以占用更少空间。例如,
(f32, u8)
元组只需 5 字节,但
size_of::<(f32, u8)>()
为 8,因为
align_of::<(f32, u8)>()
为 4。
对于未确定大小的类型,其大小和对齐取决于具体值。给定未确定大小值的引用,
std::mem::size_of_val
和
std::mem::align_of_val
函数可返回该值的大小和对齐。这些函数可处理
Sized
和未确定大小类型的引用,示例如下:
// Fat pointers to slices carry their referent's length.
let slice: &[i32] = &[1, 3, 9, 27, 81];
assert_eq!(std::mem::size_of_val(slice), 20);
let text: &str = "alligator";
assert_eq!(std::mem::size_of_val(text), 9);
use std::fmt::Display;
let unremarkable: &dyn Display = &193_u8;
let remarkable: &dyn Display = &0.0072973525664;
// These return the size/alignment of the value the
// trait object points to, not those of the trait object
// itself. This information comes from the vtable the
// trait object refers to.
assert_eq!(std::mem::size_of_val(unremarkable), 1);
assert_eq!(std::mem::align_of_val(remarkable), 8);
3. 指针算术
Rust 将数组、切片或向量的元素存储在连续的内存块中。如果每个元素占用
size
字节,那么第
i
个元素从第
i * size
字节开始。这带来了一些好处:
- 若有两个指向数组元素的原始指针,比较指针与比较元素索引结果相同。例如,若
i < j
,则指向第
i
个元素的原始指针小于指向第
j
个元素的原始指针,这使得原始指针可用于数组遍历的边界。标准库中切片的简单迭代器最初定义如下:
struct Iter<'a, T> {
ptr: *const T,
end: *const T,
...
}
ptr
字段指向迭代应产生的下一个元素,
end
字段作为迭代结束的界限,当
ptr == end
时,迭代完成。
- 若
element_ptr
是指向数组第
i
个元素的
*const T
或
*mut T
原始指针,那么
element_ptr.offset(o)
是指向第
(i + o)
个元素的原始指针,其定义等价于:
fn offset<T>(ptr: *const T, count: isize) -> *const T
where T: Sized
{
let bytes_per_element = std::mem::size_of::<T>() as isize;
let byte_offset = count * bytes_per_element;
(ptr as isize).checked_add(byte_offset).unwrap() as *const T
}
可以创建指向数组末尾后第一个字节的指针,虽不能解引用该指针,但它可用于表示循环界限或进行边界检查。不过,使用
offset
产生超出数组末尾或在数组起始之前的指针是未定义行为。若需要超出数组界限偏移指针,可使用
wrapping_offset
方法。
4. 内存移动
在实现管理自身内存的类型时,需要跟踪内存中哪些部分包含有效数据,哪些部分未初始化,就像 Rust 处理局部变量一样。例如:
let pot = "pasta".to_string();
let plate = pot;
赋值后,
pot
未初始化,
plate
成为字符串的所有者。对于管理自身内存的数据结构也是如此,如:
let mut noodles = vec!["udon".to_string()];
let soba = "soba".to_string();
let last;
向量
noodles
有额外容量,但内容是未初始化的。当执行
noodles.push(soba);
时,向量将未初始化的内存转换为新元素,
soba
变为未初始化。当执行
last = noodles.pop().unwrap();
时,
last
获得字符串所有权,向量长度减 1,该位置变为未初始化。
Rust 提供了两个基本操作来实现管理自身内存的类型:
-
std::ptr::read(src)
:将
src
指向位置的值移出,将所有权转移给调用者。
src
应为
*const T
原始指针,
T
为
Sized
类型。调用后,
*src
内容不受影响,但除非
T
是
Copy
类型,否则必须将其视为未初始化内存。这是
Vec::pop
的底层操作。
-
std::ptr::write(dest, value)
:将
value
移动到
dest
指向的位置,该位置在调用前必须是未初始化内存。
dest
必须是
*mut T
原始指针,
value
是
T
类型的值,这是
Vec::push
的底层操作。
标准库还提供了用于在不同内存块之间移动数组值的函数,如
std::ptr::copy
、
ptr.copy_to
、
std::ptr::copy_nonoverlapping
和
ptr.copy_to_nonoverlapping
。此外,
std::ptr
模块中还有
read_unaligned
、
write_unaligned
、
read_volatile
和
write_volatile
函数。
5. GapBuffer 示例
在编写文本编辑器时,若使用
String
类型,在大文件开头插入或删除字符可能会很昂贵。Emacs 文本编辑器使用的间隙缓冲区(Gap Buffer)可以在常数时间内插入和删除字符。间隙缓冲区将空闲容量保留在文本中间编辑位置,该空闲容量称为间隙。在间隙处插入或删除元素成本较低,只需根据需要缩小或扩大间隙。
以下是
GapBuffer
的实现:
use std;
use std::ops::Range;
pub struct GapBuffer<T> {
// Storage for elements. This has the capacity we need, but its length
// always remains zero. GapBuffer puts its elements and the gap in this
// `Vec`'s "unused" capacity.
storage: Vec<T>,
// Range of uninitialized elements in the middle of `storage`.
// Elements before and after this range are always initialized.
gap: Range<usize>
}
impl<T> GapBuffer<T> {
pub fn new() -> GapBuffer<T> {
GapBuffer { storage: Vec::new(), gap: 0..0 }
}
/// Return the number of elements this GapBuffer could hold without
/// reallocation.
pub fn capacity(&self) -> usize {
self.storage.capacity()
}
/// Return the number of elements this GapBuffer currently holds.
pub fn len(&self) -> usize {
self.capacity() - self.gap.len()
}
/// Return the current insertion position.
pub fn position(&self) -> usize {
self.gap.start
}
/// Return a pointer to the `index`th element of the underlying storage,
/// regardless of the gap.
///
/// Safety: `index` must be a valid index into `self.storage`.
unsafe fn space(&self, index: usize) -> *const T {
self.storage.as_ptr().offset(index as isize)
}
/// Return a mutable pointer to the `index`th element of the underlying
/// storage, regardless of the gap.
///
/// Safety: `index` must be a valid index into `self.storage`.
unsafe fn space_mut(&mut self, index: usize) -> *mut T {
self.storage.as_mut_ptr().offset(index as isize)
}
/// Return the offset in the buffer of the `index`th element, taking
/// the gap into account. This does not check whether index is in range,
/// but it never returns an index in the gap.
fn index_to_raw(&self, index: usize) -> usize {
if index < self.gap.start {
index
} else {
index + self.gap.len()
}
}
/// Return a reference to the `index`th element,
/// or `None` if `index` is out of bounds.
pub fn get(&self, index: usize) -> Option<&T> {
let raw = self.index_to_raw(index);
if raw < self.capacity() {
unsafe {
// We just checked `raw` against self.capacity(),
// and index_to_raw skips the gap, so this is safe.
Some(&*self.space(raw))
}
} else {
None
}
}
/// Set the current insertion position to `pos`.
/// If `pos` is out of bounds, panic.
pub fn set_position(&mut self, pos: usize) {
if pos > self.len() {
panic!("index {} out of range for GapBuffer", pos);
}
unsafe {
let gap = self.gap.clone();
if pos > gap.start {
// `pos` falls after the gap. Move the gap right
// by shifting elements after the gap to before it.
let distance = pos - gap.start;
std::ptr::copy(self.space(gap.end),
self.space_mut(gap.start),
distance);
} else if pos < gap.start {
// `pos` falls before the gap. Move the gap left
// by shifting elements before the gap to after it.
let distance = gap.start - pos;
std::ptr::copy(self.space(pos),
self.space_mut(gap.end - distance),
distance);
}
self.gap = pos .. pos + gap.len();
}
}
/// Insert `elt` at the current insertion position,
/// and leave the insertion position after it.
pub fn insert(&mut self, elt: T) {
if self.gap.len() == 0 {
self.enlarge_gap();
}
unsafe {
let index = self.gap.start;
std::ptr::write(self.space_mut(index), elt);
}
self.gap.start += 1;
}
/// Insert the elements produced by `iter` at the current insertion
/// position, and leave the insertion position after them.
pub fn insert_iter<I>(&mut self, iterable: I)
where I: IntoIterator<Item=T>
{
for item in iterable {
self.insert(item);
}
}
/// Remove the element just after the insertion position
/// and return it, or return `None` if the insertion position
/// is at the end of the GapBuffer.
pub fn remove(&mut self) -> Option<T> {
if self.gap.end == self.capacity() {
return None;
}
let element = unsafe {
std::ptr::read(self.space(self.gap.end))
};
self.gap.end += 1;
Some(element)
}
/// Double the capacity of `self.storage`.
fn enlarge_gap(&mut self) {
let mut new_capacity = self.capacity() * 2;
if new_capacity == 0 {
// The existing vector is empty.
// Choose a reasonable starting capacity.
new_capacity = 4;
}
// We have no idea what resizing a Vec does with its "unused"
// capacity. So just create a new vector and move over the elements.
let mut new = Vec::with_capacity(new_capacity);
let after_gap = self.capacity() - self.gap.end;
let new_gap = self.gap.start .. new.capacity() - after_gap;
unsafe {
// Move the elements that fall before the gap.
std::ptr::copy_nonoverlapping(self.space(0),
new.as_mut_ptr(),
self.gap.start);
// Move the elements that fall after the gap.
let new_gap_end = new.as_mut_ptr().offset(new_gap.end as isize);
std::ptr::copy_nonoverlapping(self.space(self.gap.end),
new_gap_end,
after_gap);
}
self.storage = new;
self.gap = new_gap;
}
}
以下是使用
GapBuffer
的示例:
let mut buf = GapBuffer::new();
buf.insert_iter("Lord of the Rings".chars());
buf.set_position(12);
buf.insert_iter("Onion ".chars());
总结
本文介绍了 Rust 中的内存操作,包括指针操作、类型大小与对齐、内存移动等知识,并通过实现
GapBuffer
展示了如何运用这些知识来优化文本编辑操作。
GapBuffer
利用原始指针和内存操作,在插入和删除字符时具有较高的效率,尤其适用于文本编辑器等场景。
通过这些操作和示例,我们可以看到 Rust 在内存管理方面的强大功能和灵活性,同时也需要注意在使用不安全代码时遵循相关规则,避免出现未定义行为。
Rust 内存操作与 GapBuffer 实现
5. GapBuffer 实现细节深入分析
在前面我们已经介绍了
GapBuffer
的基本结构和使用示例,接下来深入分析其实现细节。
5.1 核心字段与初始化
GapBuffer
结构体有两个核心字段:
pub struct GapBuffer<T> {
storage: Vec<T>,
gap: Range<usize>
}
-
storage:一个Vec<T>类型的向量,用于存储元素。但它的长度始终为 0,只是通过Vec::with_capacity(n)获取一块足够大的内存块,通过向量的as_ptr和as_mut_ptr方法获取原始指针,直接使用这块内存。 -
gap:一个Range<usize>类型的范围,表示storage中间未初始化元素的范围。该范围前后的元素始终是已初始化的。
初始化方法
new
如下:
pub fn new() -> GapBuffer<T> {
GapBuffer { storage: Vec::new(), gap: 0..0 }
}
此方法创建一个新的
GapBuffer
,初始时
storage
为空向量,
gap
范围为 0 到 0。
5.2 基本属性方法
-
capacity:返回GapBuffer在不重新分配内存的情况下可以容纳的元素数量,实际上是storage的容量。
pub fn capacity(&self) -> usize {
self.storage.capacity()
}
-
len:返回GapBuffer当前持有的元素数量,通过capacity减去gap的长度得到。
pub fn len(&self) -> usize {
self.capacity() - self.gap.len()
}
-
position:返回当前插入位置,即gap的起始位置。
pub fn position(&self) -> usize {
self.gap.start
}
5.3 元素访问方法
-
space和space_mut:分别返回指向底层存储中第index个元素的常量指针和可变指针。需要注意的是,index必须是self.storage的有效索引。
unsafe fn space(&self, index: usize) -> *const T {
self.storage.as_ptr().offset(index as isize)
}
unsafe fn space_mut(&mut self, index: usize) -> *mut T {
self.storage.as_mut_ptr().offset(index as isize)
}
-
index_to_raw:考虑gap的存在,返回第index个元素在缓冲区中的偏移量。如果index小于gap的起始位置,则直接返回index;否则,返回index + gap.len()。
fn index_to_raw(&self, index: usize) -> usize {
if index < self.gap.start {
index
} else {
index + self.gap.len()
}
}
-
get:返回第index个元素的引用,如果index越界则返回None。
pub fn get(&self, index: usize) -> Option<&T> {
let raw = self.index_to_raw(index);
if raw < self.capacity() {
unsafe {
Some(&*self.space(raw))
}
} else {
None
}
}
5.4 插入和删除操作
-
set_position:将当前插入位置设置为pos。如果pos越界,则会触发panic。根据pos与gap起始位置的关系,移动gap到新位置。
pub fn set_position(&mut self, pos: usize) {
if pos > self.len() {
panic!("index {} out of range for GapBuffer", pos);
}
unsafe {
let gap = self.gap.clone();
if pos > gap.start {
let distance = pos - gap.start;
std::ptr::copy(self.space(gap.end),
self.space_mut(gap.start),
distance);
} else if pos < gap.start {
let distance = gap.start - pos;
std::ptr::copy(self.space(pos),
self.space_mut(gap.end - distance),
distance);
}
self.gap = pos .. pos + gap.len();
}
}
-
insert:在当前插入位置插入一个元素elt。如果gap长度为 0,则调用enlarge_gap方法扩大间隙。
pub fn insert(&mut self, elt: T) {
if self.gap.len() == 0 {
self.enlarge_gap();
}
unsafe {
let index = self.gap.start;
std::ptr::write(self.space_mut(index), elt);
}
self.gap.start += 1;
}
-
insert_iter:插入一个可迭代对象中的所有元素。
pub fn insert_iter<I>(&mut self, iterable: I)
where I: IntoIterator<Item=T>
{
for item in iterable {
self.insert(item);
}
}
-
remove:移除当前插入位置之后的元素并返回它,如果插入位置在GapBuffer的末尾,则返回None。
pub fn remove(&mut self) -> Option<T> {
if self.gap.end == self.capacity() {
return None;
}
let element = unsafe {
std::ptr::read(self.space(self.gap.end))
};
self.gap.end += 1;
Some(element)
}
5.5 间隙扩大方法
enlarge_gap
方法用于在
gap
被填满时,将
storage
的容量翻倍。
fn enlarge_gap(&mut self) {
let mut new_capacity = self.capacity() * 2;
if new_capacity == 0 {
new_capacity = 4;
}
let mut new = Vec::with_capacity(new_capacity);
let after_gap = self.capacity() - self.gap.end;
let new_gap = self.gap.start .. new.capacity() - after_gap;
unsafe {
std::ptr::copy_nonoverlapping(self.space(0),
new.as_mut_ptr(),
self.gap.start);
let new_gap_end = new.as_mut_ptr().offset(new_gap.end as isize);
std::ptr::copy_nonoverlapping(self.space(self.gap.end),
new_gap_end,
after_gap);
}
self.storage = new;
self.gap = new_gap;
}
6. GapBuffer 操作流程总结
为了更清晰地理解
GapBuffer
的操作流程,我们可以用 mermaid 流程图来表示:
graph TD;
A[创建 GapBuffer] --> B[插入元素];
B --> C{是否需要移动插入位置};
C -- 是 --> D[移动 gap 到新位置];
C -- 否 --> E[继续插入或删除元素];
E --> F{gap 是否填满};
F -- 是 --> G[扩大 gap];
F -- 否 --> E;
D --> E;
G --> E;
7. 性能分析与应用场景
GapBuffer
在插入和删除元素时具有较好的性能,尤其是在频繁在中间位置进行插入和删除操作的场景下。与普通的
String
类型相比,
String
在中间插入或删除元素时需要移动大量元素,时间复杂度较高;而
GapBuffer
只需要调整
gap
的位置,时间复杂度较低。
以下是
GapBuffer
和
String
在插入操作上的性能对比表格:
| 操作类型 | 数据结构 | 时间复杂度 |
| ---- | ---- | ---- |
| 中间插入 |
String
| O(n) |
| 中间插入 |
GapBuffer
| O(1)(不考虑扩大间隙) |
GapBuffer
适用于文本编辑器、实时文本处理等场景,在这些场景中,用户经常需要在文本中间进行插入和删除操作。
8. 总结与注意事项
通过本文,我们详细介绍了 Rust 中的内存操作,包括指针算术、内存移动等,并且深入分析了
GapBuffer
的实现。
GapBuffer
利用 Rust 的原始指针和内存操作,在插入和删除元素时具有较高的效率,为文本编辑等场景提供了一种高效的数据结构。
在使用 Rust 的不安全代码时,需要特别注意以下几点:
- 确保指针操作不会导致未定义行为,如使用
offset
方法时不要超出数组边界。
- 正确管理内存的初始化和未初始化状态,避免使用未初始化的内存。
- 在使用
std::ptr::read
和
std::ptr::write
等函数时,遵循其使用规则,确保内存操作的正确性。
总之,Rust 提供了强大的内存管理功能,但也需要开发者谨慎使用,以确保代码的安全性和性能。
超级会员免费看
1342

被折叠的 条评论
为什么被折叠?



