CppCon 2017 学习:folly::Function A Non-copyable Alternative to std::function
你说的内容是关于 C++ 中 可调用类型(Callable Types) 的基础知识,我帮你理清并补充理解。
Callable Types(可调用类型)简介
C++ 中任何可以用 ()
括号操作符“调用”的对象,都叫做 可调用类型。典型包括:
1. 函数指针和函数引用
int (*functionPointer)(std::string); // 指向函数的指针
int (&functionReference)(std::string); // 函数的引用
它们都指向一个可被调用的函数,调用方式如:
int result = functionPointer("hello");
int result2 = functionReference("world");
2. Lambda 表达式
匿名函数对象,语法:
auto lambda = [](std::string s) -> int { return s.size(); };
int result = lambda("test");
lambda 实际上是编译器生成的匿名类对象,重载了 operator()
。
3. 函数对象(Functor)
你可以自定义一个类或结构体,实现 operator()
,使其实例可以像函数一样被调用:
class ComplexObject {
public:int operator()(std::string s) {return s.size() * 2; // 举例}
};
ComplexObject obj;
int result = obj("hello");
4. 补充:static
operator()
注意,你提到的:
class ComplexObject {static int operator()(std::string);
};
这个声明是不合法的。因为 operator()
必须是实例成员函数(non-static),它的意义就是让对象可以被调用。
如果你写 static
,就不能通过对象调用了:
ComplexObject obj;
obj("hello"); // 不行,static operator() 无法用obj调用
你可以写普通静态成员函数,但那就不是函数调用运算符重载了。
小结
类型 | 示例 | 说明 |
---|---|---|
函数指针 | int (*fp)(std::string); | 指向普通函数 |
函数引用 | int (&fr)(std::string); | 绑定到函数 |
Lambda 表达式 | auto lambda = [](std::string) { ... }; | 匿名函数对象,重载了 operator() |
函数对象(重载 operator() ) | class Foo { int operator()(std::string); }; | 自定义可调用类型 |
带状态的可调用对象(Stateful Callables),让我帮你详细解释和梳理下:
Stateful Callables(有状态的可调用对象)
“有状态”指的是这个可调用对象内部保存了数据(状态),每次调用可能用到这些状态或改变它们。
1. 捕获变量的 Lambda(Stateful Lambda)
int x = 5;
auto lambda = [x](std::string s) { return s.size() + x; };
- 这里的
lambda
捕获了外部变量x
,把它存到 lambda 对象里。 - 每次调用
lambda
,都会用这个x
参与计算。 - 这个 lambda 对象内部有状态(
x
),所以是 stateful callable。
2. 自定义类或结构体,重载非静态 operator()
class ComplexObject {int x;
public:ComplexObject(int x_) : x(x_) {}int operator()(std::string s) const {return s.size() + x;}
};
- 这里
ComplexObject
有成员变量x
(状态) - 重载了
operator()
,通过对象调用时可以用到内部状态。 - 也是一个 stateful callable。
3. 为什么 operator()
要是非静态?
operator()
是调用对象的核心,非静态才能访问对象的成员变量(状态)。- 静态
operator()
没法访问对象成员,因此不算真正的“stateful callable”。
4. 总结
类型 | 例子 | 特点 |
---|---|---|
无状态 Lambda | [](int x){ return x*x; } | 没捕获外部变量,纯函数 |
有状态 Lambda | [x](std::string s){ return s.size() + x; } | 捕获外部变量,内部有状态 |
函数对象(类/结构体) | 有成员变量的类重载非静态 operator() | 内部保存状态,调用时可用 |
这部分讲的是 会修改自身状态的可调用对象(State-Mutating Callables),我帮你详细说明:
State-Mutating Callables(会改变状态的可调用对象)
这类 callable 不仅“有状态”,而且在调用时会修改它们内部的状态。
1. mutable lambda
默认情况下,捕获外部变量的 lambda 是 const
调用的,不能修改捕获的变量副本。
int x = 0;
auto lambda = [x](std::string s) mutable {x += s.size(); // 修改了 lambda 内部捕获的 x 副本return x;
};
mutable
关键字让 lambda 的operator()
变成非 const,允许修改捕获的变量副本。- 注意,捕获的是值拷贝,修改的是 lambda 内部的那个副本,不影响外部
x
。
2. 非 const 的非静态 operator()
class ComplexObject {int x = 0;
public:int operator()(std::string s) {x += s.size(); // 修改对象的状态return x;}
};
- 这里
operator()
不是const
,说明调用会改变对象状态。 - 你可以记录调用次数、累计某些值等。
3. 区别于前面 const operator()
的地方
const operator()
不允许修改成员变量(除非用mutable
修饰成员变量)- 非 const
operator()
可以修改成员变量,实现状态变化
4. 总结表
类型 | 示例 | 说明 |
---|---|---|
mutable lambda | [x](std::string s) mutable { x += s.size(); return x; } | 允许修改捕获变量的副本 |
类的非 const operator() | int operator()(std::string s) { x += s.size(); return x; } | 允许修改对象成员变量,状态改变 |
你提到的是传递可调用对象(callables)给函数的方式,特别是用 函数指针 传递的限制,我帮你详细解释:
传递可调用对象(Passing Callables)
1. 函数指针只能传递无状态的可调用对象
举例:
std::string work(int x);
void workAsynchronously(int x, void (*processResult)(std::string));
processResult
是一个 函数指针,指向一个void(std::string)
的函数。- 这意味着传入的回调只能是无状态的普通函数,不能是有状态的 lambda 或函数对象。
2. 为什么函数指针只能指向无状态函数?
- 函数指针实际指向具体的函数地址。
- 有状态 lambda 和函数对象是对象实例,它们保存状态,需要调用其成员函数(
operator()
),而不是普通函数地址。 - 因此,函数指针无法表示带状态的可调用对象。
3. 如果想传递有状态 callable,该怎么办?
- 用
std::function
(类型擦除),支持任何可调用对象,包括有状态的 lambda 和函数对象:
#include <functional>
void workAsynchronously(int x, std::function<void(std::string)> processResult);
- 这样,传入的
processResult
可以是:
auto lambda = [capturedData](std::string s) { /*...*/ };
workAsynchronously(5, lambda);
4. 总结
传递方式 | 支持的 callable 类型 | 备注 |
---|---|---|
函数指针(void(*)(T) ) | 只能无状态的普通函数 | 不能传递捕获变量的 lambda |
std::function<T> | 支持所有 callable,包括带状态的 lambda 和函数对象 | 灵活但有一定运行时开销 |
std::function
其实是个 函数包装器(Function Wrapper),用来封装各种可调用对象,提供统一调用接口。下面帮你拆解理解:
std::function
简化结构解析
template<typename R, typename... Args>
class function<R(Args...)> {void* state; // 存储可调用对象的状态(捕获的数据)void (*func)(void*, Args...); // 指向“调用”函数的指针,传入state和参数void (*destroy)(void*); // 指向销毁函数,用于清理state指针内存~function(); // 析构时调用destroy清理stateR operator()(Args... args); // 调用包装的函数对象template<typename F>function(F&& f); // 构造时,把任意可调用对象包装进来template<typename F>function& operator=(F&& f); // 赋值操作符,重新包装
};
关键点
- void state*
这里保存了任意类型的可调用对象的状态,比如捕获的变量、类实例等等。
用void*
实现类型擦除,隐藏了具体类型。 - func 指针
指向一个统一的调用接口,实现了调用时把state
传进去,再调用对应可调用对象的operator()
。 - destroy 指针
用于析构state
指向的对象,防止内存泄漏。 - 构造函数模板
允许用任意类型的可调用对象(函数指针、lambda、函数对象)来初始化std::function
。 - 调用运算符
让std::function
实例看起来像普通函数一样,可以直接用()
调用。
为什么需要这么设计?
- 统一接口:不管是普通函数、带状态的 lambda,还是自定义的函数对象,都能用同一种类型变量来保存并调用。
- 类型擦除:隐藏不同可调用对象类型的差异,简化代码调用。
- 灵活性和便利性:把多态调用变得简单。
你提到的是 std::function
的实现细节和其运行时开销,我来帮你详细解析这些点,帮助你更好理解:
std::function
的结构(以 libstdc++ 的实现为例)
一个 std::function<R(Args...)>
实例,大约占 48 字节(在 x86_64 上),这些字节大致分配如下:
组成部分 | 说明 |
---|---|
函数调用指针 | 指向调用封装对象的函数:R (*invoke)(void*, Args...) |
管理函数指针 | 指向处理 copy、destroy、move 的函数:void (*manager)(...) |
内部 buffer(32B) | 小对象优化区,用于直接放入小对象(例如小 lambda) |
或者指针指向堆内对象 | 如果封装对象较大,就会动态分配在堆上,buffer 存储的是指针地址 |
特性解析
小对象优化(SBO:Small Buffer Optimization)
- 如果你的 lambda 或函数对象小于 32 字节,就会直接放入内部 buffer,避免堆分配,提高性能。
- 如果太大,就会在堆上创建对象,然后内部 buffer 存放指针。
可复制(copyable)
std::function
是可复制的。- 拷贝会拷贝封装的对象(调用对应的
copy
管理函数)。
std::function<void()> f1 = [] { std::puts("hi"); };
std::function<void()> f2 = f1; // 拷贝了 lambda 封装体
不一定 noexcept 可移动
- 移动操作不是
noexcept
,意味着在某些容器(如std::vector
)中移动std::function
可能触发重新分配或异常传播。 - 原因:封装的对象类型可能自带抛异常的移动构造函数。
示例:大小对比
#include <iostream>
#include <functional>
int main() {std::function<void()> small = [] { std::cout << "small\n"; };auto bigLambda = [buffer = std::array<int, 100>{}] { std::cout << "big\n"; };std::function<void()> big = bigLambda;std::cout << "sizeof(std::function): " << sizeof(std::function<void()>) << "\n";
}
输出(在 x86_64 上):
sizeof(std::function): 48
总结
特性 | 说明 |
---|---|
大小 | 通常 48 字节(libstdc++) |
SBO | 32 字节内的对象放栈上,否则放堆上 |
可复制 | 会复制被封装对象 |
移动非 noexcept | 使用时注意异常安全性 |
功能 | 封装任意可调用对象,延迟调用,多态调用 |
你说的这段是在说明 std::function
的典型使用场景,尤其是它的任务封装功能。我来帮你逐句解释:
典型用途解释
「passing a task to libraries for execution at a later time」
将某个任务(比如 lambda)传给一个库,让它**“以后”执行**,不是现在马上执行。
例子:传一个回调到异步网络库中。
void startAsync(std::function<void(std::string)> callback);
「or in a different thread」
把任务传到另一个线程去执行,比如线程池。
例子:
std::thread t([] { doWork(); });
「storing those tasks in the library implementations」
这些任务(lambda、函数等)通常会被**“保存”在库内部的数据结构里**,等时间合适再执行。
例子:任务队列
std::queue<std::function<void()>> taskQueue;
「in either case, those tasks are never executed more than once」
这些任务通常只执行一次,执行完就丢弃,没有重用的需求。
所以不需要支持多次调用,比如:
auto task = [] { std::puts("run once"); };
task(); //
task(); // 通常不会有这个需求
「and there is never a need to copy them」
这些任务传进去之后,不会被复制。只需要“移动”进库中,然后库调用它就结束了。
总结成一句话:
这些“任务式”的可调用对象通常:
- 只执行一次
- 被移动而非复制
- 用于延迟/异步执行
- 常用于线程池、事件循环、任务调度器中
延伸建议:使用 std::function
还是 std::move
或 std::unique_function
如果任务只用一次、无需复制,std::function
有点重。C++23 起可以用 std::move_only_function
(或者第三方的 unique_function
),更轻更高效:
std::move_only_function<void()> task = std::move(lambda);
你这段是对 Facebook 的开源代码库(如 Folly)中,std::function
的典型使用场景的描述。我来帮你逐条解释:
内容逐句解析
folly::Executor* executor;
这是一种抽象接口指针,用来表示一个“任务执行器”。
executor->add(callable);
往这个执行器里“添加”一个任务(可调用对象)。callable
可以是 lambda、函数、绑定对象等。
folly::Executor is an interface (abstract base class) for passing tasks that need to be executed
folly::Executor
是一个接口类(抽象基类),它的职责是:接收并执行异步任务。
相当于你设计了一个标准协议,任何执行器(线程池、事件循环等)都可以实现它。
implementations include a thread pool which executes tasks in parallel
这个接口的具体实现包括:
- 线程池(并行执行)
- 单线程事件循环(串行调度)
- IO 线程(和 Reactor 模式结合)
std::function<void()> was used to pass tasks to the executor
std::function<void()>
是传给 executor->add()
的参数类型,表示:
“一个可以执行、不带参数、不返回值的任务”。
这种使用方式让
folly::Executor
成为一个高层、通用的任务调度器接口。
总结
在 Facebook/Folly 的实践中:
元素 | 作用 |
---|---|
folly::Executor | 任务执行接口(类似抽象线程池) |
executor->add(task) | 添加一个延迟执行的任务 |
std::function<void()> | 用来封装传入的任务 |
设计上的好处
- 任务的来源不受限制(lambda、函数、类)
- 执行方式可以灵活替换(线程池、主线程调度器)
- 接口通用、便于解耦模块
这段讲的是 Facebook Folly 库中 Future
的常见使用场景,以及它与 std::function
的关系。我来帮你逐句解析 + 总结背后原理:
示例代码含义
folly::Future<std::string> result = someRpcCall(1, 2, 3);
表示调用了一个异步 RPC 函数,它返回一个 Future<std::string>
,未来会获得一个 std::string
。
result.then([&foo](std::string r) {return foo.extractNumber(r);
})
给这个 Future
添加一个 .then()
回调:
- 当
result
可用了,就调用 lambda。 - lambda 从字符串里提取出数字(
int
)。
.then([obj = std::move(obj)](int x) {obj.setNumber(x);
});
接着链式 .then()
调用,把前一个结果 x
(一个 int
)交给另一个 lambda。
- 这里用了 C++ 的“带 move capture 的 lambda”
obj
是只在 lambda 中使用的一个局部状态对象
概念解释:Future + then 回调机制
Future<T>
:代表未来会得到一个 T
这是一个异步结果占位符。
.then(func)
:
当结果可用时,调用你传进去的 func(回调)。
就像 JavaScript 的
.then()
,但类型安全,支持 C++ 特性。
回调的存储方式:使用 std::function
你写的这句:
“the implementation used to use std::function to store the callback”
表示早期实现是这样写的:
std::function<void(T)> callback;
也就是用 std::function
来存储 then()
传入的 lambda。这有几个好处:
优点 | 缺点 |
---|---|
可以存储任意可调用对象 | 比较重(拷贝 / 类型擦除) |
简化了接口 | 不支持 move-only 类型 |
后续优化:不再使用 std::function
因为 std::function
不支持:
- move-only lambda(捕获 unique_ptr 或 std::move(obj))
- noexcept move
- 精确类型推导(性能)
所以 Folly 后来换成了 手写的轻量 type-erased function wrapper,支持 move-only 语义。类似于:
template<typename T>
struct MoveOnlyCallback {virtual void operator()(T) = 0;virtual ~MoveOnlyCallback() = default;
};
总结
这段代码展示了:
内容 | 意义 |
---|---|
Future<T> | 管理异步结果 |
.then(callback) | 注册异步回调 |
回调传 lambda(可带状态) | 支持链式操作、异步数据流 |
早期用 std::function 储存回调 | 简单但不支持 move-only,后来被优化掉 |
你这段是讲 C++ 中使用 std::function
的限制,尤其是它不支持捕获 move-only 类型的问题,以及 Facebook Folly 提供的一些 解决方法。下面是逐点讲解与深入理解:
问题:std::function
不支持 move-only 捕获
示例问题代码:
MoveOnlyType x;
executor.add([x = std::move(x)]() mutable { x.doStuff(); });
这在某些实现中无法编译,原因是:
std::function
只能封装可复制(copyable)对象。而这个 lambda 捕获了MoveOnlyType
,它不可复制。
常见 Workaround 1:用 std::shared_ptr
auto x = std::make_shared<MoveOnlyType>();
executor.add([x]() { x->doStuff(); });
优点:
- lambda 可复制了,shared_ptr 也是可复制的
缺点:
- 每次调用都涉及堆分配
- 需要原子操作维护引用计数(性能差)
- 会让你为避免
std::function
限制而牺牲所有权语义
Workaround 2:用 folly::MoveWrapper<T>
folly::MoveWrapper<MoveOnlyType> x;
executor.add([x]() mutable { x->doStuff(); });
优点:
MoveWrapper<T>
实际上是一个**“伪复制对象”**,在复制时会自动移动内部对象- 这让 lambda 看起来是可复制的,但实际上把资源从左值转成了右值传进来
缺点:
- 违反了 C++ 的复制语义(拷贝其实是 move)
- 很像已经弃用的
std::auto_ptr
—— 危险、易错 - 一不小心就可能在拷贝时丢失数据
本质问题总结:
项目 | 问题 |
---|---|
std::function | 不能 wrap move-only lambda |
lambda 捕获 move-only | 会使 lambda 本身不可复制 |
std::function 要求 copyable | 所以编译报错 |
workaround(shared_ptr / MoveWrapper) | 都是权衡性能或语义的方案 |
Folly 的真正解决方案(后续)
Folly 的后续优化是引入了一个可以支持 move-only lambda 的轻量函数包装器(非 std::function
),其特性包括:
- 支持
unique_ptr
、Promise
等 move-only 类型 - 支持
noexcept
move - 避免拷贝构造限制
- 可以零堆分配(small buffer optimization)
小结
方法 | 优点 | 缺点 |
---|---|---|
std::function | 通用、简洁 | 不支持 move-only 捕获 |
shared_ptr | 兼容 std::function | 堆分配、性能差、共享所有权 |
folly::MoveWrapper | 可变通使用 | 破坏复制语义、易出错 |
自定义轻量函数包装器(如 Folly) | 真正解决 move-only 问题 | 实现复杂,不是标准库 |
为什么需要一种不同的 Function Wrapper
std::function
的核心问题:
要求所有可调用对象(callables)是可拷贝的(copyable)
这对很多实际用例来说,是一种不必要的限制,尤其是:
我们并不需要拷贝这些可调用对象
- 比如在线程池中提交任务时:
- 只会执行一次(one-shot)
- 不需要拷贝(只需要 move 进去,然后调用)
- 可调用对象中经常有
unique_ptr
、Promise
这类 move-only 类型
结果是:
你不能直接使用 lambda 捕获 move-only 对象:
MoveOnlyType x;
executor.add([x = std::move(x)]() mutable { x.doStuff(); }); // std::function 不接受
- 捕获了
MoveOnlyType
,lambda 本身就变成 move-only - 而
std::function<void()>
要求构造函数参数是 copyable
所以你真正想要的是:
一个轻量级的 function wrapper,可以 wrap:
- move-only 的 lambda
- 只 move、不 copy
- 小对象无需堆分配
- 只执行一次(one-shot callable)也没问题
Facebook 的 Folly 库就因此创造了:
folly::Function
:是 move-only 的 function wrapper- 用于线程池、异步任务、promise 等现代用例
- 避免了不必要的性能开销
小结:
标准库 std::function | 实际开发常见需求 |
---|---|
需要可复制 callable | 只需要 move、执行一次 |
可能导致堆分配 | 想要 small buffer 或 zero allocation |
不支持 move-only 捕获 | 现代 C++ 任务常用 move-only 对象 |
如果你要构建一个支持 MoveOnly 的任务系统,那使用 std::function 是不合适的。你应该考虑: |
- 自己实现一个简易的
MoveFunction
- 或者使用 Folly 的
folly::Function
关于 const
正确性(Const Correctness)在 std::function
中的一个重要细节
你可能会以为:
std::function<void()> f;
void someFunction(const std::function<void()>& f) {f(); // f 是 const,调用 f() 应该不会修改内部状态
}
表面上看,f()
是 const
的成员函数,所以你以为:
- 包装的 lambda 或函数对象也应该是 const 调用
- 内部状态不会被修改
实际上:
R operator()(Args...) const;
这个 operator()
是 const
没错,但是:
- 它内部调用的是
(*callable)(args...)
,这个 callable 是void*
转型来的 - 没有检查实际 wrapped 对象的 constness!
所以问题来了:
std::function<void()> f = [x = 0]() mutable { /* 修改 x */ };
f(); // 可以正常调用,虽然是 const 对象
即使 f
是 const
,只要里面包装的是 mutable lambda
或非 const operator()
的对象:
仍然可以修改状态!
总结就是:
看起来 | 实际上 |
---|---|
operator() 是 const | 但 wrapped callable 可能是非-const 的 |
你以为不改状态 | 实际上可以修改状态(比如 mutable lambda) |
所以这段话的重点理解是:
std::function::operator()
是const
,但它没有真正地保证 const-correctness。
你不能依赖它来保证你的代码不会修改内部状态。
你这段内容是讲解 folly::Function
的设计目的和内部实现机制,下面是逐点的理解与总结:
folly::Function
的设计动机与特点
1. Non-copyable(不可拷贝)
- 为什么?
要支持捕获unique_ptr
、Promise
等move-only 的类型 std::function
的问题: 只支持可拷贝类型folly::Function
的解决方案:
自己就是不可拷贝的(copy constructor & copy assignment 被删除)
folly::Function
使用 值语义(value semantics),但通过move
实现转移所有权
2. noexcept-Movable
- 如果你定义了一个类型含有
folly::Function
成员变量,
STL 要求你这个类型如果想noexcept move
,那成员也必须是noexcept move
- 所以
folly::Function
本身实现了noexcept move
,以便:
支持std::move_if_noexcept
与 STL 容器、线程池等类型协作良好
3. Const Correct
folly::Function
有两个版本:folly::Function<void()> // 非 const operator() folly::Function<void() const> // const operator()
- 保证 const-correctness,解决
std::function
的历史问题 - 如果你用的是:
那里面包的 callable 也必须支持 const 调用!const folly::Function<void()>& f; f(); // 调用的是 const operator()
4. 实现细节(Implementation Details)
特性 | 说明 |
---|---|
大小 | 64 bytes(x86_64 架构) |
调用指针 | 1 个指针:真正的调用函数 |
管理函数指针 | 用于 move、destroy 等操作 |
小对象优化 | 提供 48 字节的 inline 存储空间 |
大对象策略 | 不满足 noexcept-movable 的对象将 heap 分配 |
哪些对象不能 inline 存储?
- 非
noexcept
movable 的对象
会退化为堆分配(使用指针 + heap 管理)
设计目标小结:
目标 | 实现方式 |
---|---|
支持 move-only 对象 | 类型本身不可拷贝 |
高性能 | 小对象优化,避免堆分配 |
STL 容器兼容性 | noexcept move |
保证 const 正确性 | 两个签名:void() vs void() const |
这部分内容总结了 folly::Function
相比 std::function
的优势、迁移实践、适用场景以及性能表现。下面是对这些内容的逐条解释与理解:
Trivia:互操作性
std::function
可以转换为folly::Function
(拷贝构造)folly::Function
不能转换为std::function
,因为std::function
需要拷贝构造,而folly::Function
是不可拷贝的
迁移到 folly::Function
大多数情况下可作为 drop-in 替代品
但也有例外:
- 如果代码 依赖拷贝语义(很少见)
- 如果代码 依赖不正确的 const 使用(那是个 bug,
folly::Function
会强制你修)
使用方式差异:
std::function
常常以const&
传参
folly::Function
不可拷贝,必须以&
或&&
传参
Facebook 内部采用
- 在
folly::Future
中替换了std::function
带来最大受益:可使用 move-only 的回调 - 在
folly::Executor
中替换std::function
需要修改很多子类,但修改过程常揭示原有代码问题
何时使用 std::function vs folly::Function
使用场景 | 推荐 |
---|---|
需要复制 callable 的 API | std::function |
一般回调/异步任务/只需移动语义 | folly::Function |
使用 MoveWrapper 等拷贝模拟方式 | 不要这样做,使用 folly::Function 替代更安全清晰 |
Benchmark(性能)
调用方式 | 时间(越小越好) | 每秒调用次数(越高越好) |
---|---|---|
函数指针调用 (fn_ptr ) | ~1.3 ns | ~761M 次 |
std::function 调用 | ~2.28 ns | ~437M 次 |
folly::Function 调用 | ~1.96 ns | ~510M 次 |
std::function 创建 + 调用 | ~3.04 ns | ~329M 次 |
folly::Function 创建 + 调用 | ~2.79 ns | ~359M 次 |
folly::Function 通常快于 std::function |
总结
点 | 内容 |
---|---|
目标 | 替代 std::function ,支持 move-only 类型 |
不支持 | 拷贝(故不能做 const& 参数) |
迁移建议 | 改成 & 或 && 传参 |
效果 | 避免 ugly workarounds(如 MoveWrapper 、shared_ptr 等) |
性能 | 与 std::function 持平或更快 |
应用 | 在 Facebook 内部已广泛部署 |