http://wiki.fnil.net/api.php?action=feedcontributions&user=Dennis+zhuang&feedformat=atom
Dennis的知识库 - 用户贡献 [zh-cn]
2024-03-29T11:35:14Z
用户贡献
MediaWiki 1.20.1
http://wiki.fnil.net/index.php?title=Mongodb_%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0
Mongodb 源码阅读笔记
2017-05-26T03:36:32Z
<p>Dennis zhuang:</p>
<hr />
<div><br />
== 网络层 ==<br />
<br />
* 具体实现在 src/mongo/util/net 目录下:<br />
<br />
<pre><br />
├── abstract_message_port.h<br />
├── asio_message_port.cpp<br />
├── asio_message_port.h<br />
├── asio_ssl_context.cpp<br />
├── asio_ssl_context.h<br />
├── hostandport.cpp<br />
├── hostandport.h<br />
├── hostandport_test.cpp<br />
├── hostname_canonicalization.cpp<br />
├── hostname_canonicalization.h<br />
├── listen.cpp<br />
├── listen.h<br />
├── message.cpp<br />
├── message.h<br />
├── message_port.cpp<br />
├── message_port.h<br />
├── message_port_mock.cpp<br />
├── message_port_mock.h<br />
├── message_port_startup_param.cpp<br />
├── message_port_startup_param.h<br />
├── op_msg.cpp<br />
├── op_msg.h<br />
├── sock.cpp<br />
├── sock.h<br />
├── sock_test.cpp<br />
├── sockaddr.cpp<br />
├── sockaddr.h<br />
├── socket_exception.cpp<br />
├── socket_exception.h<br />
├── socket_poll.cpp<br />
├── socket_poll.h<br />
├── ssl_expiration.cpp<br />
├── ssl_expiration.h<br />
├── ssl_manager.cpp<br />
├── ssl_manager.h<br />
├── ssl_options.cpp<br />
├── ssl_options.h<br />
├── ssl_types.h<br />
├── thread_idle_callback.cpp<br />
└── thread_idle_callback.h<br />
</pre><br />
<br />
* 针对上层提供的服务接口定义在 src/mongo/transport 下,核心就是 transport_layer , transport_layer_legacy 和 service_entry_point,具体的关系是 TransportLayer 持有一个 acceptor ,当连接进来,包装成 session,然后调用ServiceEntryPoint.startSession 方法,进入一个读取请求-处理请求-应答请求的循环。 这里可能为了兼容老的代码,transport_layer_legacy 实现了 TransportLayer,并兼容老的代码。 ServiceEntryPoint 的子类 ServiceEntryPointImpl 里实现了 startSession 和 _sessionLoop 框架,每个连接启动一个线程处理:<br />
<br />
<pre><br />
//service_entry_point_impl.cpp<br />
void ServiceEntryPointImpl::startSession(transport::SessionHandle session) {<br />
// Pass ownership of the transport::SessionHandle into our worker thread. When this<br />
// thread exits, the session will end.<br />
launchWrappedServiceEntryWorkerThread(<br />
std::move(session), [this](const transport::SessionHandle& session) {<br />
_nWorkers.fetchAndAdd(1);<br />
auto guard = MakeGuard([&] { _nWorkers.fetchAndSubtract(1); });<br />
<br />
_sessionLoop(session);<br />
});<br />
}<br />
<br />
void ServiceEntryPointImpl::_sessionLoop(const transport::SessionHandle& session) {<br />
Message inMessage;<br />
bool inExhaust = false;<br />
int64_t counter = 0;<br />
<br />
while (true) {<br />
.........<br />
// The handleRequest is implemented in a subclass for mongod/mongos and actually all the<br />
// database work for this request.<br />
DbResponse dbresponse = this->handleRequest(opCtx.get(), inMessage, session->remote());<br />
......<br />
}<br />
}<br />
</pre><br />
<br />
实际是转交给 ServiceEntryPointImpl 的子类 mongod 和 mongos 的 handleRequest 处理请求。<br />
<br />
* transport_layer_legacy.cpp 就是调用 util/net 下面的类和方法,实现一个典型的 TCP 服务器了,其中 handleNewConnection 处理新建连接,构造函数里开始 listen:<br />
<br />
<pre><br />
TransportLayerLegacy::TransportLayerLegacy(const TransportLayerLegacy::Options& opts,<br />
ServiceEntryPoint* sep)<br />
: _sep(sep),<br />
_listener(stdx::make_unique<ListenerLegacy>(<br />
opts,<br />
stdx::bind(&TransportLayerLegacy::_handleNewConnection, this, stdx::placeholders::_1))),<br />
_running(false),<br />
_options(opts) {}<br />
<br />
void TransportLayerLegacy::_handleNewConnection(std::unique_ptr<AbstractMessagingPort> amp) {<br />
if (!Listener::globalTicketHolder.tryAcquire()) {<br />
log() << "connection refused because too many open connections: "<br />
<< Listener::globalTicketHolder.used();<br />
amp->shutdown();<br />
return;<br />
}<br />
<br />
amp->setLogLevel(logger::LogSeverity::Debug(1));<br />
auto session = LegacySession::create(std::move(amp), this);<br />
<br />
stdx::list<std::weak_ptr<LegacySession>> list;<br />
auto it = list.emplace(list.begin(), session);<br />
<br />
{<br />
// Add the new session to our list<br />
stdx::lock_guard<stdx::mutex> lk(_sessionsMutex);<br />
session->setIter(it);<br />
_sessions.splice(_sessions.begin(), list, it);<br />
}<br />
<br />
invariant(_sep);<br />
_sep->startSession(std::move(session));<br />
}<br />
<br />
</pre><br />
<br />
调用了 _sep->startSession(std::move(session));,其中 _sep 就是 ServiceEntryPoint 指针,在头文件 class 定义了。<br />
<br />
* 网络层来看, accept 采用了 select 调用,读写请求还是同步阻塞的过程。<br />
<br />
== db 层 ==<br />
<br />
* db 这一层源码都在 src/mongo/db/ 下,其中 commands 对应各种命令, storage 是各种存储引擎,比如 MMAPv1, wiredtiger 等, query 就是专门用于查询的。ops 里面是各种写入(插入、更新和删除等)的 ops 实现,query 专门处理查询,catalog 管理 db 和 collection。<br />
* mongod 是 ServiceEntryPointImpl 的实现,在 src/mongo/db/service_entry_point_mongod.cpp ,实现 handleRequest 方法:<br />
<br />
<pre><br />
DbResponse ServiceEntryPointMongod::handleRequest(OperationContext* opCtx,<br />
const Message& request,<br />
const HostAndPort& client) {<br />
return assembleResponse(opCtx, request, client);<br />
}<br />
<br />
</pre><br />
<br />
转交调用了 assembleResponse 方法,定义在 assemble_response.cpp 中:<br />
<br />
<pre><br />
DbResponse assembleResponse(OperationContext* opCtx, const Message& m, const HostAndPort& remote) {<br />
// before we lock...<br />
NetworkOp op = m.operation();<br />
bool isCommand = false;<br />
<br />
DbMessage dbmsg(m);<br />
<br />
if (op == dbQuery) {<br />
if (nsString.isCommand()) {<br />
isCommand = true;<br />
opwrite(m);<br />
}<br />
// TODO: remove this entire code path after 3.2. Refs SERVER-7775<br />
else if (nsString.isSpecialCommand()) {<br />
opwrite(m);<br />
<br />
if (nsString.coll() == "$cmd.sys.inprog") {<br />
return receivedPseudoCommand(opCtx, c, m, "currentOp");<br />
}<br />
if (nsString.coll() == "$cmd.sys.killop") {<br />
return receivedPseudoCommand(opCtx, c, m, "killOp");<br />
}<br />
if (nsString.coll() == "$cmd.sys.unlock") {<br />
return receivedPseudoCommand(opCtx, c, m, "fsyncUnlock");<br />
}<br />
} else {<br />
opread(m);<br />
}<br />
} else if (op == dbGetMore) {<br />
opread(m);<br />
} else if (op == dbCommand || op == dbMsg) {<br />
isCommand = true;<br />
opwrite(m);<br />
} else {<br />
opwrite(m);<br />
}<br />
<br />
CurOp& currentOp = *CurOp::get(opCtx);<br />
{<br />
stdx::lock_guard<Client> lk(*opCtx->getClient());<br />
// Commands handling code will reset this if the operation is a command<br />
// which is logically a basic CRUD operation like query, insert, etc.<br />
currentOp.setNetworkOp_inlock(op);<br />
currentOp.setLogicalOp_inlock(networkOpToLogicalOp(op));<br />
}<br />
<br />
OpDebug& debug = currentOp.debug();<br />
<br />
long long logThresholdMs = serverGlobalParams.slowMS;<br />
bool shouldLogOpDebug = shouldLog(logger::LogSeverity::Debug(1));<br />
<br />
DbResponse dbresponse;<br />
if (op == dbQuery) {<br />
dbresponse = isCommand ? receivedCommand(opCtx, nsString, c, m)<br />
: receivedQuery(opCtx, nsString, c, m);<br />
} else if (op == dbMsg) {<br />
dbresponse = receivedMsg(opCtx, c, m);<br />
} else if (op == dbCommand) {<br />
dbresponse = receivedRpc(opCtx, c, m);<br />
} else if (op == dbGetMore) {<br />
dbresponse = receivedGetMore(opCtx, m, currentOp, &shouldLogOpDebug);<br />
} else {<br />
</pre><br />
<br />
这就是一个典型的根据命令类型派发的过程,比较奇怪的是没有使用枚举+ switch 的方式,而是一堆 if .. else。<br />
<br />
派发的核心是转发 commands 和 query,分别调用的是 runCommands 和 runQuery 方法,前者定义在 run_commands.cpp,后者定义在 query/find.cpp 里,一个是各种命令,一个是查询。不过在 assembleResponse 里还有一些 receivedInsert, receivedDelete 之类的处理逻辑,看代码注释是说『// The remaining operations do not return any response. They are fire-and-forget.』,也就是这些请求都不需要应答,因此应该不是正常的插入、更新和删除的请求处理过程,具体用于什么暂时不清楚。<br />
<br />
* 插入、删除和更新过程还是 run_commands.cpp 里, 本质上在 commands.cpp 保存了一张表 ,映射了名字到具体的 command, Command 的每个具体子类都会在构造函数里将自己注册到这里。<br />
<br />
<pre><br />
<br />
//commands.h<br />
static CommandMap* _commands;<br />
static CommandMap* _commandsByBestName;<br />
<br />
<br />
//commands.cpp<br />
<br />
Command::Command(StringData name, bool webUI, StringData oldName)<br />
: _name(name.toString()),<br />
_webUI(webUI),<br />
_commandsExecutedMetric("commands." + _name + ".total", &_commandsExecuted),<br />
_commandsFailedMetric("commands." + _name + ".failed", &_commandsFailed) {<br />
// register ourself.<br />
if (_commands == 0)<br />
_commands = new CommandMap();<br />
if (_commandsByBestName == 0)<br />
_commandsByBestName = new CommandMap();<br />
Command*& c = (*_commands)[name];<br />
if (c)<br />
log() << "warning: 2 commands with name: " << _name;<br />
c = this;<br />
(*_commandsByBestName)[name] = this;<br />
<br />
if (!oldName.empty())<br />
(*_commands)[oldName.toString()] = this;<br />
}<br />
<br />
<br />
</pre><br />
<br />
比如 WriteCommand(用于数据写入,包括插入、更新删除),定义在 src/mongo/db/commands/write_commands/write_commands.cpp:<br />
<br />
<pre><br />
class WriteCommand : public Command {<br />
public:<br />
explicit WriteCommand(StringData name) : Command(name) {}<br />
<br />
</pre><br />
<br />
而插入 CmdInsert 就是 WriteCommand 的一个子类,其他更新、删除类似:<br />
<br />
<pre><br />
class CmdInsert final : public WriteCommand {<br />
public:<br />
CmdInsert() : WriteCommand("insert") {}<br />
<br />
<br />
void runImpl(OperationContext* opCtx,<br />
const std::string& dbname,<br />
const BSONObj& cmdObj,<br />
BSONObjBuilder& result) final {<br />
const auto batch = parseInsertCommand(dbname, cmdObj);<br />
const auto reply = performInserts(opCtx, batch);<br />
serializeReply(opCtx,<br />
ReplyStyle::kNotUpdate,<br />
batch.continueOnError,<br />
batch.documents.size(),<br />
reply,<br />
&result);<br />
}<br />
} cmdInsert;<br />
<br />
</pre><br />
<br />
实际的插入执行的是 performInsert 方法,定义在 src/mongo/db/ops/write_ops_exec.cpp 中:<br />
<br />
<pre><br />
WriteResult performInserts(OperationContext* opCtx, const InsertOp& wholeOp) {<br />
invariant(!opCtx->lockState()->inAWriteUnitOfWork()); // Does own retries.<br />
auto& curOp = *CurOp::get(opCtx);<br />
......<br />
for (auto&& doc : wholeOp.documents) {<br />
const bool isLastDoc = (&doc == &wholeOp.documents.back());<br />
auto fixedDoc = fixDocumentForInsert(opCtx->getServiceContext(), doc);<br />
......<br />
bool canContinue = insertBatchAndHandleErrors(opCtx, wholeOp, batch, &lastOpFixer, &out);<br />
...<br />
<br />
<br />
}<br />
</pre><br />
<br />
实际执行的是 insertBatchAndHandleErrors 方法,逻辑是比较清楚了,先找到 ns ,获取对应的 db 和 collection,然后执行 collection 的 insertDocuments 方法:<br />
<br />
<pre><br />
try {<br />
acquireCollection();<br />
....<br />
....<br />
insertDocuments(opCtx, collection->getCollection(), batch.begin(), batch.end());<br />
<br />
</pre><br />
<br />
插入对非 capped 集合做一次 all-at-once 批量插入,如果不行,再循环走一次 one-at-a-time 插入。<br />
<br />
insertDocuments 定义在 src/mongo/db/catalog/collection_impl.cpp 里,每个 collection 都保存了一个 StorageEngine 创建的 RecordStore,调用 recordStore 做插入:<br />
<br />
<pre><br />
Status CollectionImpl::_insertDocuments(OperationContext* opCtx,<br />
const vector<BSONObj>::const_iterator begin,<br />
const vector<BSONObj>::const_iterator end,<br />
bool enforceQuota,<br />
OpDebug* opDebug) {<br />
<br />
Status status = _recordStore->insertRecords(opCtx, &records, _enforceQuota(enforceQuota));<br />
if (!status.isOK())<br />
return status;<br />
<br />
status = _indexCatalog.indexRecords(opCtx, bsonRecords, &keysInserted);<br />
if (opDebug) {<br />
opDebug->keysInserted += keysInserted;<br />
}<br />
<br />
</pre><br />
<br />
可以看到除了插入数据,还要更新下索引。<br />
<br />
== 内存管理 ==<br />
<br />
* mongodb 从 2.2 开始使用 Google 的 TCMalloc 作为默认的 allocator,linux 默认就是,可以在编译的时候通过 --allocator= 来指定。https://docs.mongodb.com/v3.2/release-notes/2.2/<br />
* mongodb 在 src/mongo/util/allocator.cpp 稍微包装了下:<br />
<br />
<pre><br />
namespace mongo {<br />
<br />
void* mongoMalloc(size_t size) {<br />
void* x = std::malloc(size);<br />
if (x == NULL) {<br />
reportOutOfMemoryErrorAndExit();<br />
}<br />
return x;<br />
}<br />
<br />
void* mongoRealloc(void* ptr, size_t size) {<br />
void* x = std::realloc(ptr, size);<br />
if (x == NULL) {<br />
reportOutOfMemoryErrorAndExit();<br />
}<br />
return x;<br />
}<br />
<br />
} // namespace mongo<br />
</pre> <br />
<br />
* 一些性能测试: http://smalldatum.blogspot.jp/2015/06/insert-benchmark-for-mongodb-memory.html 和 http://smalldatum.blogspot.jp/2014/12/malloc-and-mongodb-performance.html</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0
Mongodb 源码阅读笔记
2017-05-25T03:25:23Z
<p>Dennis zhuang:/* db 层 */</p>
<hr />
<div><br />
== 网络层 ==<br />
<br />
* 具体实现在 src/mongo/util/net 目录下:<br />
<br />
<pre><br />
├── abstract_message_port.h<br />
├── asio_message_port.cpp<br />
├── asio_message_port.h<br />
├── asio_ssl_context.cpp<br />
├── asio_ssl_context.h<br />
├── hostandport.cpp<br />
├── hostandport.h<br />
├── hostandport_test.cpp<br />
├── hostname_canonicalization.cpp<br />
├── hostname_canonicalization.h<br />
├── listen.cpp<br />
├── listen.h<br />
├── message.cpp<br />
├── message.h<br />
├── message_port.cpp<br />
├── message_port.h<br />
├── message_port_mock.cpp<br />
├── message_port_mock.h<br />
├── message_port_startup_param.cpp<br />
├── message_port_startup_param.h<br />
├── op_msg.cpp<br />
├── op_msg.h<br />
├── sock.cpp<br />
├── sock.h<br />
├── sock_test.cpp<br />
├── sockaddr.cpp<br />
├── sockaddr.h<br />
├── socket_exception.cpp<br />
├── socket_exception.h<br />
├── socket_poll.cpp<br />
├── socket_poll.h<br />
├── ssl_expiration.cpp<br />
├── ssl_expiration.h<br />
├── ssl_manager.cpp<br />
├── ssl_manager.h<br />
├── ssl_options.cpp<br />
├── ssl_options.h<br />
├── ssl_types.h<br />
├── thread_idle_callback.cpp<br />
└── thread_idle_callback.h<br />
</pre><br />
<br />
* 针对上层提供的服务接口定义在 src/mongo/transport 下,核心就是 transport_layer , transport_layer_legacy 和 service_entry_point,具体的关系是 TransportLayer 持有一个 acceptor ,当连接进来,包装成 session,然后调用ServiceEntryPoint.startSession 方法,进入一个读取请求-处理请求-应答请求的循环。 这里可能为了兼容老的代码,transport_layer_legacy 实现了 TransportLayer,并兼容老的代码。 ServiceEntryPoint 的子类 ServiceEntryPointImpl 里实现了 startSession 和 _sessionLoop 框架,每个连接启动一个线程处理:<br />
<br />
<pre><br />
//service_entry_point_impl.cpp<br />
void ServiceEntryPointImpl::startSession(transport::SessionHandle session) {<br />
// Pass ownership of the transport::SessionHandle into our worker thread. When this<br />
// thread exits, the session will end.<br />
launchWrappedServiceEntryWorkerThread(<br />
std::move(session), [this](const transport::SessionHandle& session) {<br />
_nWorkers.fetchAndAdd(1);<br />
auto guard = MakeGuard([&] { _nWorkers.fetchAndSubtract(1); });<br />
<br />
_sessionLoop(session);<br />
});<br />
}<br />
<br />
void ServiceEntryPointImpl::_sessionLoop(const transport::SessionHandle& session) {<br />
Message inMessage;<br />
bool inExhaust = false;<br />
int64_t counter = 0;<br />
<br />
while (true) {<br />
.........<br />
// The handleRequest is implemented in a subclass for mongod/mongos and actually all the<br />
// database work for this request.<br />
DbResponse dbresponse = this->handleRequest(opCtx.get(), inMessage, session->remote());<br />
......<br />
}<br />
}<br />
</pre><br />
<br />
实际是转交给 ServiceEntryPointImpl 的子类 mongod 和 mongos 的 handleRequest 处理请求。<br />
<br />
* transport_layer_legacy.cpp 就是调用 util/net 下面的类和方法,实现一个典型的 TCP 服务器了,其中 handleNewConnection 处理新建连接,构造函数里开始 listen:<br />
<br />
<pre><br />
TransportLayerLegacy::TransportLayerLegacy(const TransportLayerLegacy::Options& opts,<br />
ServiceEntryPoint* sep)<br />
: _sep(sep),<br />
_listener(stdx::make_unique<ListenerLegacy>(<br />
opts,<br />
stdx::bind(&TransportLayerLegacy::_handleNewConnection, this, stdx::placeholders::_1))),<br />
_running(false),<br />
_options(opts) {}<br />
<br />
void TransportLayerLegacy::_handleNewConnection(std::unique_ptr<AbstractMessagingPort> amp) {<br />
if (!Listener::globalTicketHolder.tryAcquire()) {<br />
log() << "connection refused because too many open connections: "<br />
<< Listener::globalTicketHolder.used();<br />
amp->shutdown();<br />
return;<br />
}<br />
<br />
amp->setLogLevel(logger::LogSeverity::Debug(1));<br />
auto session = LegacySession::create(std::move(amp), this);<br />
<br />
stdx::list<std::weak_ptr<LegacySession>> list;<br />
auto it = list.emplace(list.begin(), session);<br />
<br />
{<br />
// Add the new session to our list<br />
stdx::lock_guard<stdx::mutex> lk(_sessionsMutex);<br />
session->setIter(it);<br />
_sessions.splice(_sessions.begin(), list, it);<br />
}<br />
<br />
invariant(_sep);<br />
_sep->startSession(std::move(session));<br />
}<br />
<br />
</pre><br />
<br />
调用了 _sep->startSession(std::move(session));,其中 _sep 就是 ServiceEntryPoint 指针,在头文件 class 定义了。<br />
<br />
* 网络层来看, accept 采用了 select 调用,读写请求还是同步阻塞的过程。<br />
<br />
== db 层 ==<br />
<br />
* db 这一层源码都在 src/mongo/db/ 下,其中 commands 对应各种命令, storage 是各种存储引擎,比如 MMAPv1, wiredtiger 等, query 就是专门用于查询的。ops 里面是各种写入(插入、更新和删除等)的 ops 实现,query 专门处理查询,catalog 管理 db 和 collection。<br />
* mongod 是 ServiceEntryPointImpl 的实现,在 src/mongo/db/service_entry_point_mongod.cpp ,实现 handleRequest 方法:<br />
<br />
<pre><br />
DbResponse ServiceEntryPointMongod::handleRequest(OperationContext* opCtx,<br />
const Message& request,<br />
const HostAndPort& client) {<br />
return assembleResponse(opCtx, request, client);<br />
}<br />
<br />
</pre><br />
<br />
转交调用了 assembleResponse 方法,定义在 assemble_response.cpp 中:<br />
<br />
<pre><br />
DbResponse assembleResponse(OperationContext* opCtx, const Message& m, const HostAndPort& remote) {<br />
// before we lock...<br />
NetworkOp op = m.operation();<br />
bool isCommand = false;<br />
<br />
DbMessage dbmsg(m);<br />
<br />
if (op == dbQuery) {<br />
if (nsString.isCommand()) {<br />
isCommand = true;<br />
opwrite(m);<br />
}<br />
// TODO: remove this entire code path after 3.2. Refs SERVER-7775<br />
else if (nsString.isSpecialCommand()) {<br />
opwrite(m);<br />
<br />
if (nsString.coll() == "$cmd.sys.inprog") {<br />
return receivedPseudoCommand(opCtx, c, m, "currentOp");<br />
}<br />
if (nsString.coll() == "$cmd.sys.killop") {<br />
return receivedPseudoCommand(opCtx, c, m, "killOp");<br />
}<br />
if (nsString.coll() == "$cmd.sys.unlock") {<br />
return receivedPseudoCommand(opCtx, c, m, "fsyncUnlock");<br />
}<br />
} else {<br />
opread(m);<br />
}<br />
} else if (op == dbGetMore) {<br />
opread(m);<br />
} else if (op == dbCommand || op == dbMsg) {<br />
isCommand = true;<br />
opwrite(m);<br />
} else {<br />
opwrite(m);<br />
}<br />
<br />
CurOp& currentOp = *CurOp::get(opCtx);<br />
{<br />
stdx::lock_guard<Client> lk(*opCtx->getClient());<br />
// Commands handling code will reset this if the operation is a command<br />
// which is logically a basic CRUD operation like query, insert, etc.<br />
currentOp.setNetworkOp_inlock(op);<br />
currentOp.setLogicalOp_inlock(networkOpToLogicalOp(op));<br />
}<br />
<br />
OpDebug& debug = currentOp.debug();<br />
<br />
long long logThresholdMs = serverGlobalParams.slowMS;<br />
bool shouldLogOpDebug = shouldLog(logger::LogSeverity::Debug(1));<br />
<br />
DbResponse dbresponse;<br />
if (op == dbQuery) {<br />
dbresponse = isCommand ? receivedCommand(opCtx, nsString, c, m)<br />
: receivedQuery(opCtx, nsString, c, m);<br />
} else if (op == dbMsg) {<br />
dbresponse = receivedMsg(opCtx, c, m);<br />
} else if (op == dbCommand) {<br />
dbresponse = receivedRpc(opCtx, c, m);<br />
} else if (op == dbGetMore) {<br />
dbresponse = receivedGetMore(opCtx, m, currentOp, &shouldLogOpDebug);<br />
} else {<br />
</pre><br />
<br />
这就是一个典型的根据命令类型派发的过程,比较奇怪的是没有使用枚举+ switch 的方式,而是一堆 if .. else。<br />
<br />
派发的核心是转发 commands 和 query,分别调用的是 runCommands 和 runQuery 方法,前者定义在 run_commands.cpp,后者定义在 query/find.cpp 里,一个是各种命令,一个是查询。不过在 assembleResponse 里还有一些 receivedInsert, receivedDelete 之类的处理逻辑,看代码注释是说『// The remaining operations do not return any response. They are fire-and-forget.』,也就是这些请求都不需要应答,因此应该不是正常的插入、更新和删除的请求处理过程,具体用于什么暂时不清楚。<br />
<br />
* 插入、删除和更新过程还是 run_commands.cpp 里, 本质上在 commands.cpp 保存了一张表 ,映射了名字到具体的 command, Command 的每个具体子类都会在构造函数里将自己注册到这里。<br />
<br />
<pre><br />
<br />
//commands.h<br />
static CommandMap* _commands;<br />
static CommandMap* _commandsByBestName;<br />
<br />
<br />
//commands.cpp<br />
<br />
Command::Command(StringData name, bool webUI, StringData oldName)<br />
: _name(name.toString()),<br />
_webUI(webUI),<br />
_commandsExecutedMetric("commands." + _name + ".total", &_commandsExecuted),<br />
_commandsFailedMetric("commands." + _name + ".failed", &_commandsFailed) {<br />
// register ourself.<br />
if (_commands == 0)<br />
_commands = new CommandMap();<br />
if (_commandsByBestName == 0)<br />
_commandsByBestName = new CommandMap();<br />
Command*& c = (*_commands)[name];<br />
if (c)<br />
log() << "warning: 2 commands with name: " << _name;<br />
c = this;<br />
(*_commandsByBestName)[name] = this;<br />
<br />
if (!oldName.empty())<br />
(*_commands)[oldName.toString()] = this;<br />
}<br />
<br />
<br />
</pre><br />
<br />
比如 WriteCommand(用于数据写入,包括插入、更新删除),定义在 src/mongo/db/commands/write_commands/write_commands.cpp:<br />
<br />
<pre><br />
class WriteCommand : public Command {<br />
public:<br />
explicit WriteCommand(StringData name) : Command(name) {}<br />
<br />
</pre><br />
<br />
而插入 CmdInsert 就是 WriteCommand 的一个子类,其他更新、删除类似:<br />
<br />
<pre><br />
class CmdInsert final : public WriteCommand {<br />
public:<br />
CmdInsert() : WriteCommand("insert") {}<br />
<br />
<br />
void runImpl(OperationContext* opCtx,<br />
const std::string& dbname,<br />
const BSONObj& cmdObj,<br />
BSONObjBuilder& result) final {<br />
const auto batch = parseInsertCommand(dbname, cmdObj);<br />
const auto reply = performInserts(opCtx, batch);<br />
serializeReply(opCtx,<br />
ReplyStyle::kNotUpdate,<br />
batch.continueOnError,<br />
batch.documents.size(),<br />
reply,<br />
&result);<br />
}<br />
} cmdInsert;<br />
<br />
</pre><br />
<br />
实际的插入执行的是 performInsert 方法,定义在 src/mongo/db/ops/write_ops_exec.cpp 中:<br />
<br />
<pre><br />
WriteResult performInserts(OperationContext* opCtx, const InsertOp& wholeOp) {<br />
invariant(!opCtx->lockState()->inAWriteUnitOfWork()); // Does own retries.<br />
auto& curOp = *CurOp::get(opCtx);<br />
......<br />
for (auto&& doc : wholeOp.documents) {<br />
const bool isLastDoc = (&doc == &wholeOp.documents.back());<br />
auto fixedDoc = fixDocumentForInsert(opCtx->getServiceContext(), doc);<br />
......<br />
bool canContinue = insertBatchAndHandleErrors(opCtx, wholeOp, batch, &lastOpFixer, &out);<br />
...<br />
<br />
<br />
}<br />
</pre><br />
<br />
实际执行的是 insertBatchAndHandleErrors 方法,逻辑是比较清楚了,先找到 ns ,获取对应的 db 和 collection,然后执行 collection 的 insertDocuments 方法:<br />
<br />
<pre><br />
try {<br />
acquireCollection();<br />
....<br />
....<br />
insertDocuments(opCtx, collection->getCollection(), batch.begin(), batch.end());<br />
<br />
</pre><br />
<br />
插入对非 capped 集合做一次 all-at-once 批量插入,如果不行,再循环走一次 one-at-a-time 插入。<br />
<br />
insertDocuments 定义在 src/mongo/db/catalog/collection_impl.cpp 里,每个 collection 都保存了一个 StorageEngine 创建的 RecordStore,调用 recordStore 做插入:<br />
<br />
<pre><br />
Status CollectionImpl::_insertDocuments(OperationContext* opCtx,<br />
const vector<BSONObj>::const_iterator begin,<br />
const vector<BSONObj>::const_iterator end,<br />
bool enforceQuota,<br />
OpDebug* opDebug) {<br />
<br />
Status status = _recordStore->insertRecords(opCtx, &records, _enforceQuota(enforceQuota));<br />
if (!status.isOK())<br />
return status;<br />
<br />
status = _indexCatalog.indexRecords(opCtx, bsonRecords, &keysInserted);<br />
if (opDebug) {<br />
opDebug->keysInserted += keysInserted;<br />
}<br />
<br />
</pre><br />
<br />
可以看到除了插入数据,还要更新下索引。</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0
Mongodb 源码阅读笔记
2017-05-25T03:05:45Z
<p>Dennis zhuang:</p>
<hr />
<div><br />
== 网络层 ==<br />
<br />
* 具体实现在 src/mongo/util/net 目录下:<br />
<br />
<pre><br />
├── abstract_message_port.h<br />
├── asio_message_port.cpp<br />
├── asio_message_port.h<br />
├── asio_ssl_context.cpp<br />
├── asio_ssl_context.h<br />
├── hostandport.cpp<br />
├── hostandport.h<br />
├── hostandport_test.cpp<br />
├── hostname_canonicalization.cpp<br />
├── hostname_canonicalization.h<br />
├── listen.cpp<br />
├── listen.h<br />
├── message.cpp<br />
├── message.h<br />
├── message_port.cpp<br />
├── message_port.h<br />
├── message_port_mock.cpp<br />
├── message_port_mock.h<br />
├── message_port_startup_param.cpp<br />
├── message_port_startup_param.h<br />
├── op_msg.cpp<br />
├── op_msg.h<br />
├── sock.cpp<br />
├── sock.h<br />
├── sock_test.cpp<br />
├── sockaddr.cpp<br />
├── sockaddr.h<br />
├── socket_exception.cpp<br />
├── socket_exception.h<br />
├── socket_poll.cpp<br />
├── socket_poll.h<br />
├── ssl_expiration.cpp<br />
├── ssl_expiration.h<br />
├── ssl_manager.cpp<br />
├── ssl_manager.h<br />
├── ssl_options.cpp<br />
├── ssl_options.h<br />
├── ssl_types.h<br />
├── thread_idle_callback.cpp<br />
└── thread_idle_callback.h<br />
</pre><br />
<br />
* 针对上层提供的服务接口定义在 src/mongo/transport 下,核心就是 transport_layer , transport_layer_legacy 和 service_entry_point,具体的关系是 TransportLayer 持有一个 acceptor ,当连接进来,包装成 session,然后调用ServiceEntryPoint.startSession 方法,进入一个读取请求-处理请求-应答请求的循环。 这里可能为了兼容老的代码,transport_layer_legacy 实现了 TransportLayer,并兼容老的代码。 ServiceEntryPoint 的子类 ServiceEntryPointImpl 里实现了 startSession 和 _sessionLoop 框架,每个连接启动一个线程处理:<br />
<br />
<pre><br />
//service_entry_point_impl.cpp<br />
void ServiceEntryPointImpl::startSession(transport::SessionHandle session) {<br />
// Pass ownership of the transport::SessionHandle into our worker thread. When this<br />
// thread exits, the session will end.<br />
launchWrappedServiceEntryWorkerThread(<br />
std::move(session), [this](const transport::SessionHandle& session) {<br />
_nWorkers.fetchAndAdd(1);<br />
auto guard = MakeGuard([&] { _nWorkers.fetchAndSubtract(1); });<br />
<br />
_sessionLoop(session);<br />
});<br />
}<br />
<br />
void ServiceEntryPointImpl::_sessionLoop(const transport::SessionHandle& session) {<br />
Message inMessage;<br />
bool inExhaust = false;<br />
int64_t counter = 0;<br />
<br />
while (true) {<br />
.........<br />
// The handleRequest is implemented in a subclass for mongod/mongos and actually all the<br />
// database work for this request.<br />
DbResponse dbresponse = this->handleRequest(opCtx.get(), inMessage, session->remote());<br />
......<br />
}<br />
}<br />
</pre><br />
<br />
实际是转交给 ServiceEntryPointImpl 的子类 mongod 和 mongos 的 handleRequest 处理请求。<br />
<br />
* transport_layer_legacy.cpp 就是调用 util/net 下面的类和方法,实现一个典型的 TCP 服务器了,其中 handleNewConnection 处理新建连接,构造函数里开始 listen:<br />
<br />
<pre><br />
TransportLayerLegacy::TransportLayerLegacy(const TransportLayerLegacy::Options& opts,<br />
ServiceEntryPoint* sep)<br />
: _sep(sep),<br />
_listener(stdx::make_unique<ListenerLegacy>(<br />
opts,<br />
stdx::bind(&TransportLayerLegacy::_handleNewConnection, this, stdx::placeholders::_1))),<br />
_running(false),<br />
_options(opts) {}<br />
<br />
void TransportLayerLegacy::_handleNewConnection(std::unique_ptr<AbstractMessagingPort> amp) {<br />
if (!Listener::globalTicketHolder.tryAcquire()) {<br />
log() << "connection refused because too many open connections: "<br />
<< Listener::globalTicketHolder.used();<br />
amp->shutdown();<br />
return;<br />
}<br />
<br />
amp->setLogLevel(logger::LogSeverity::Debug(1));<br />
auto session = LegacySession::create(std::move(amp), this);<br />
<br />
stdx::list<std::weak_ptr<LegacySession>> list;<br />
auto it = list.emplace(list.begin(), session);<br />
<br />
{<br />
// Add the new session to our list<br />
stdx::lock_guard<stdx::mutex> lk(_sessionsMutex);<br />
session->setIter(it);<br />
_sessions.splice(_sessions.begin(), list, it);<br />
}<br />
<br />
invariant(_sep);<br />
_sep->startSession(std::move(session));<br />
}<br />
<br />
</pre><br />
<br />
调用了 _sep->startSession(std::move(session));,其中 _sep 就是 ServiceEntryPoint 指针,在头文件 class 定义了。<br />
<br />
* 网络层来看, accept 采用了 select 调用,读写请求还是同步阻塞的过程。<br />
<br />
== db 层 ==<br />
<br />
* db 这一层源码都在 src/mongo/db/ 下,其中 commands 对应各种命令, storage 是各种存储引擎,比如 MMAPv1, wiredtiger 等, query 就是专门用于查询的。<br />
* mongod 是 ServiceEntryPointImpl 的实现,在 src/mongo/db/service_entry_point_mongod.cpp ,实现 handleRequest 方法:<br />
<br />
<pre><br />
DbResponse ServiceEntryPointMongod::handleRequest(OperationContext* opCtx,<br />
const Message& request,<br />
const HostAndPort& client) {<br />
return assembleResponse(opCtx, request, client);<br />
}<br />
<br />
</pre><br />
<br />
转交调用了 assembleResponse 方法,定义在 assemble_response.cpp 中:<br />
<br />
<pre><br />
DbResponse assembleResponse(OperationContext* opCtx, const Message& m, const HostAndPort& remote) {<br />
// before we lock...<br />
NetworkOp op = m.operation();<br />
bool isCommand = false;<br />
<br />
DbMessage dbmsg(m);<br />
<br />
if (op == dbQuery) {<br />
if (nsString.isCommand()) {<br />
isCommand = true;<br />
opwrite(m);<br />
}<br />
// TODO: remove this entire code path after 3.2. Refs SERVER-7775<br />
else if (nsString.isSpecialCommand()) {<br />
opwrite(m);<br />
<br />
if (nsString.coll() == "$cmd.sys.inprog") {<br />
return receivedPseudoCommand(opCtx, c, m, "currentOp");<br />
}<br />
if (nsString.coll() == "$cmd.sys.killop") {<br />
return receivedPseudoCommand(opCtx, c, m, "killOp");<br />
}<br />
if (nsString.coll() == "$cmd.sys.unlock") {<br />
return receivedPseudoCommand(opCtx, c, m, "fsyncUnlock");<br />
}<br />
} else {<br />
opread(m);<br />
}<br />
} else if (op == dbGetMore) {<br />
opread(m);<br />
} else if (op == dbCommand || op == dbMsg) {<br />
isCommand = true;<br />
opwrite(m);<br />
} else {<br />
opwrite(m);<br />
}<br />
<br />
CurOp& currentOp = *CurOp::get(opCtx);<br />
{<br />
stdx::lock_guard<Client> lk(*opCtx->getClient());<br />
// Commands handling code will reset this if the operation is a command<br />
// which is logically a basic CRUD operation like query, insert, etc.<br />
currentOp.setNetworkOp_inlock(op);<br />
currentOp.setLogicalOp_inlock(networkOpToLogicalOp(op));<br />
}<br />
<br />
OpDebug& debug = currentOp.debug();<br />
<br />
long long logThresholdMs = serverGlobalParams.slowMS;<br />
bool shouldLogOpDebug = shouldLog(logger::LogSeverity::Debug(1));<br />
<br />
DbResponse dbresponse;<br />
if (op == dbQuery) {<br />
dbresponse = isCommand ? receivedCommand(opCtx, nsString, c, m)<br />
: receivedQuery(opCtx, nsString, c, m);<br />
} else if (op == dbMsg) {<br />
dbresponse = receivedMsg(opCtx, c, m);<br />
} else if (op == dbCommand) {<br />
dbresponse = receivedRpc(opCtx, c, m);<br />
} else if (op == dbGetMore) {<br />
dbresponse = receivedGetMore(opCtx, m, currentOp, &shouldLogOpDebug);<br />
} else {<br />
</pre><br />
<br />
这就是一个典型的根据命令类型派发的过程,比较奇怪的是没有使用枚举+ switch 的方式,而是一堆 if .. else。<br />
<br />
派发的核心是转发 commands 和 query,分别调用的是 runCommands 和 runQuery 方法,前者定义在 run_commands.cpp,后者定义在 query/find.cpp 里,一个是各种命令,一个是查询。不过在 assembleResponse 里还有一些 receivedInsert, receivedDelete 之类的处理逻辑,看代码注释是说『// The remaining operations do not return any response. They are fire-and-forget.』,也就是这些请求都不需要应答,因此应该不是正常的插入、更新和删除的请求处理过程,具体用于什么暂时不清楚。<br />
<br />
* 插入、删除和更新过程还是 run_commands.cpp 里, 本质上在 commands.cpp 保存了一张表 ,映射了名字到具体的 command, Command 的每个具体子类都会在构造函数里将自己注册到这里。<br />
<br />
<pre><br />
<br />
//commands.h<br />
static CommandMap* _commands;<br />
static CommandMap* _commandsByBestName;<br />
<br />
<br />
//commands.cpp<br />
<br />
Command::Command(StringData name, bool webUI, StringData oldName)<br />
: _name(name.toString()),<br />
_webUI(webUI),<br />
_commandsExecutedMetric("commands." + _name + ".total", &_commandsExecuted),<br />
_commandsFailedMetric("commands." + _name + ".failed", &_commandsFailed) {<br />
// register ourself.<br />
if (_commands == 0)<br />
_commands = new CommandMap();<br />
if (_commandsByBestName == 0)<br />
_commandsByBestName = new CommandMap();<br />
Command*& c = (*_commands)[name];<br />
if (c)<br />
log() << "warning: 2 commands with name: " << _name;<br />
c = this;<br />
(*_commandsByBestName)[name] = this;<br />
<br />
if (!oldName.empty())<br />
(*_commands)[oldName.toString()] = this;<br />
}<br />
<br />
<br />
</pre><br />
<br />
比如 WriteCommand(用于数据写入,包括插入、更新删除),定义在 src/mongo/db/commands/write_commands/write_commands.cpp:<br />
<br />
<pre><br />
class WriteCommand : public Command {<br />
public:<br />
explicit WriteCommand(StringData name) : Command(name) {}<br />
<br />
</pre><br />
<br />
而插入 CmdInsert 就是 WriteCommand 的一个子类,其他更新、删除类似:<br />
<br />
<pre><br />
class CmdInsert final : public WriteCommand {<br />
public:<br />
CmdInsert() : WriteCommand("insert") {}<br />
<br />
<br />
void runImpl(OperationContext* opCtx,<br />
const std::string& dbname,<br />
const BSONObj& cmdObj,<br />
BSONObjBuilder& result) final {<br />
const auto batch = parseInsertCommand(dbname, cmdObj);<br />
const auto reply = performInserts(opCtx, batch);<br />
serializeReply(opCtx,<br />
ReplyStyle::kNotUpdate,<br />
batch.continueOnError,<br />
batch.documents.size(),<br />
reply,<br />
&result);<br />
}<br />
} cmdInsert;<br />
<br />
</pre><br />
<br />
实际的插入执行的是 performInsert 方法,定义在 src/mongo/db/ops/write_ops_exec.cpp 中:<br />
<br />
<pre><br />
WriteResult performInserts(OperationContext* opCtx, const InsertOp& wholeOp) {<br />
invariant(!opCtx->lockState()->inAWriteUnitOfWork()); // Does own retries.<br />
auto& curOp = *CurOp::get(opCtx);<br />
......<br />
for (auto&& doc : wholeOp.documents) {<br />
const bool isLastDoc = (&doc == &wholeOp.documents.back());<br />
auto fixedDoc = fixDocumentForInsert(opCtx->getServiceContext(), doc);<br />
......<br />
bool canContinue = insertBatchAndHandleErrors(opCtx, wholeOp, batch, &lastOpFixer, &out);<br />
...<br />
<br />
<br />
}<br />
</pre><br />
<br />
实际执行的是 insertBatchAndHandleErrors 方法,逻辑是比较清楚了,先找到 ns ,获取对应的 db 和 collection,然后执行 collection 的 insertDocuments 方法:<br />
<br />
<pre><br />
try {<br />
acquireCollection();<br />
....<br />
....<br />
insertDocuments(opCtx, collection->getCollection(), batch.begin(), batch.end());<br />
<br />
</pre><br />
<br />
插入对非 capped 集合做一次 all-at-once 批量插入,如果不行,再循环走一次 one-at-a-time 插入。</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0
Mongodb 源码阅读笔记
2017-05-24T07:42:33Z
<p>Dennis zhuang:/* 网络层 */</p>
<hr />
<div><br />
== 网络层 ==<br />
<br />
* 具体实现在 src/mongo/util/net 目录下:<br />
<br />
<pre><br />
├── abstract_message_port.h<br />
├── asio_message_port.cpp<br />
├── asio_message_port.h<br />
├── asio_ssl_context.cpp<br />
├── asio_ssl_context.h<br />
├── hostandport.cpp<br />
├── hostandport.h<br />
├── hostandport_test.cpp<br />
├── hostname_canonicalization.cpp<br />
├── hostname_canonicalization.h<br />
├── listen.cpp<br />
├── listen.h<br />
├── message.cpp<br />
├── message.h<br />
├── message_port.cpp<br />
├── message_port.h<br />
├── message_port_mock.cpp<br />
├── message_port_mock.h<br />
├── message_port_startup_param.cpp<br />
├── message_port_startup_param.h<br />
├── op_msg.cpp<br />
├── op_msg.h<br />
├── sock.cpp<br />
├── sock.h<br />
├── sock_test.cpp<br />
├── sockaddr.cpp<br />
├── sockaddr.h<br />
├── socket_exception.cpp<br />
├── socket_exception.h<br />
├── socket_poll.cpp<br />
├── socket_poll.h<br />
├── ssl_expiration.cpp<br />
├── ssl_expiration.h<br />
├── ssl_manager.cpp<br />
├── ssl_manager.h<br />
├── ssl_options.cpp<br />
├── ssl_options.h<br />
├── ssl_types.h<br />
├── thread_idle_callback.cpp<br />
└── thread_idle_callback.h<br />
</pre><br />
<br />
* 针对上层提供的服务接口定义在 src/mongo/transport 下,核心就是 transport_layer , transport_layer_legacy 和 service_entry_point,具体的关系是 TransportLayer 持有一个 acceptor ,当连接进来,包装成 session,然后调用ServiceEntryPoint.startSession 方法,进入一个读取请求-处理请求-应答请求的循环。 这里可能为了兼容老的代码,transport_layer_legacy 实现了 TransportLayer,并兼容老的代码。 ServiceEntryPoint 的子类 ServiceEntryPointImpl 里实现了 startSession 和 _sessionLoop 框架,每个连接启动一个线程处理:<br />
<br />
<pre><br />
//service_entry_point_impl.cpp<br />
void ServiceEntryPointImpl::startSession(transport::SessionHandle session) {<br />
// Pass ownership of the transport::SessionHandle into our worker thread. When this<br />
// thread exits, the session will end.<br />
launchWrappedServiceEntryWorkerThread(<br />
std::move(session), [this](const transport::SessionHandle& session) {<br />
_nWorkers.fetchAndAdd(1);<br />
auto guard = MakeGuard([&] { _nWorkers.fetchAndSubtract(1); });<br />
<br />
_sessionLoop(session);<br />
});<br />
}<br />
<br />
void ServiceEntryPointImpl::_sessionLoop(const transport::SessionHandle& session) {<br />
Message inMessage;<br />
bool inExhaust = false;<br />
int64_t counter = 0;<br />
<br />
while (true) {<br />
.........<br />
// The handleRequest is implemented in a subclass for mongod/mongos and actually all the<br />
// database work for this request.<br />
DbResponse dbresponse = this->handleRequest(opCtx.get(), inMessage, session->remote());<br />
......<br />
}<br />
}<br />
</pre><br />
<br />
实际是转交给 ServiceEntryPointImpl 的子类 mongod 和 mongos 的 handleRequest 处理请求。<br />
<br />
* transport_layer_legacy.cpp 就是调用 util/net 下面的类和方法,实现一个典型的 TCP 服务器了,其中 handleNewConnection 处理新建连接,构造函数里开始 listen:<br />
<br />
<pre><br />
TransportLayerLegacy::TransportLayerLegacy(const TransportLayerLegacy::Options& opts,<br />
ServiceEntryPoint* sep)<br />
: _sep(sep),<br />
_listener(stdx::make_unique<ListenerLegacy>(<br />
opts,<br />
stdx::bind(&TransportLayerLegacy::_handleNewConnection, this, stdx::placeholders::_1))),<br />
_running(false),<br />
_options(opts) {}<br />
<br />
void TransportLayerLegacy::_handleNewConnection(std::unique_ptr<AbstractMessagingPort> amp) {<br />
if (!Listener::globalTicketHolder.tryAcquire()) {<br />
log() << "connection refused because too many open connections: "<br />
<< Listener::globalTicketHolder.used();<br />
amp->shutdown();<br />
return;<br />
}<br />
<br />
amp->setLogLevel(logger::LogSeverity::Debug(1));<br />
auto session = LegacySession::create(std::move(amp), this);<br />
<br />
stdx::list<std::weak_ptr<LegacySession>> list;<br />
auto it = list.emplace(list.begin(), session);<br />
<br />
{<br />
// Add the new session to our list<br />
stdx::lock_guard<stdx::mutex> lk(_sessionsMutex);<br />
session->setIter(it);<br />
_sessions.splice(_sessions.begin(), list, it);<br />
}<br />
<br />
invariant(_sep);<br />
_sep->startSession(std::move(session));<br />
}<br />
<br />
</pre><br />
<br />
调用了 _sep->startSession(std::move(session));,其中 _sep 就是 ServiceEntryPoint 指针,在头文件 class 定义了。<br />
<br />
* 网络层来看, accept 采用了 select 调用,读写请求还是同步阻塞的过程。<br />
<br />
== db 层 ==<br />
<br />
* db 这一层源码都在 src/mongo/db/ 下,其中 commands 对应各种命令, storage 是各种存储引擎,比如 MMAPv1, wiredtiger 等, query 就是专门用于查询的。<br />
* mongod 是 ServiceEntryPointImpl 的实现,在 src/mongo/db/service_entry_point_mongod.cpp ,实现 handleRequest 方法:<br />
<br />
<pre><br />
DbResponse ServiceEntryPointMongod::handleRequest(OperationContext* opCtx,<br />
const Message& request,<br />
const HostAndPort& client) {<br />
return assembleResponse(opCtx, request, client);<br />
}<br />
<br />
</pre><br />
<br />
转交调用了 assembleResponse 方法,定义在 assemble_response.cpp 中:<br />
<br />
<pre><br />
DbResponse assembleResponse(OperationContext* opCtx, const Message& m, const HostAndPort& remote) {<br />
// before we lock...<br />
NetworkOp op = m.operation();<br />
bool isCommand = false;<br />
<br />
DbMessage dbmsg(m);<br />
<br />
if (op == dbQuery) {<br />
if (nsString.isCommand()) {<br />
isCommand = true;<br />
opwrite(m);<br />
}<br />
// TODO: remove this entire code path after 3.2. Refs SERVER-7775<br />
else if (nsString.isSpecialCommand()) {<br />
opwrite(m);<br />
<br />
if (nsString.coll() == "$cmd.sys.inprog") {<br />
return receivedPseudoCommand(opCtx, c, m, "currentOp");<br />
}<br />
if (nsString.coll() == "$cmd.sys.killop") {<br />
return receivedPseudoCommand(opCtx, c, m, "killOp");<br />
}<br />
if (nsString.coll() == "$cmd.sys.unlock") {<br />
return receivedPseudoCommand(opCtx, c, m, "fsyncUnlock");<br />
}<br />
} else {<br />
opread(m);<br />
}<br />
} else if (op == dbGetMore) {<br />
opread(m);<br />
} else if (op == dbCommand || op == dbMsg) {<br />
isCommand = true;<br />
opwrite(m);<br />
} else {<br />
opwrite(m);<br />
}<br />
<br />
CurOp& currentOp = *CurOp::get(opCtx);<br />
{<br />
stdx::lock_guard<Client> lk(*opCtx->getClient());<br />
// Commands handling code will reset this if the operation is a command<br />
// which is logically a basic CRUD operation like query, insert, etc.<br />
currentOp.setNetworkOp_inlock(op);<br />
currentOp.setLogicalOp_inlock(networkOpToLogicalOp(op));<br />
}<br />
<br />
OpDebug& debug = currentOp.debug();<br />
<br />
long long logThresholdMs = serverGlobalParams.slowMS;<br />
bool shouldLogOpDebug = shouldLog(logger::LogSeverity::Debug(1));<br />
<br />
DbResponse dbresponse;<br />
if (op == dbQuery) {<br />
dbresponse = isCommand ? receivedCommand(opCtx, nsString, c, m)<br />
: receivedQuery(opCtx, nsString, c, m);<br />
} else if (op == dbMsg) {<br />
dbresponse = receivedMsg(opCtx, c, m);<br />
} else if (op == dbCommand) {<br />
dbresponse = receivedRpc(opCtx, c, m);<br />
} else if (op == dbGetMore) {<br />
dbresponse = receivedGetMore(opCtx, m, currentOp, &shouldLogOpDebug);<br />
} else {<br />
</pre><br />
<br />
这就是一个典型的根据命令类型派发的过程,比较奇怪的是没有使用枚举+ switch 的方式,而是一堆 if .. else。<br />
<br />
派发的核心是转发 commands 和 query,分别调用的是 runCommands 和 runQuery 方法,前者定义在 run_commands.cpp,后者定义在 query/find.cpp 里,一个是各种命令,一个是查询。</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0
Mongodb 源码阅读笔记
2017-05-24T07:32:26Z
<p>Dennis zhuang:/* 网络层 */</p>
<hr />
<div><br />
== 网络层 ==<br />
<br />
* 具体实现在 src/mongo/util/net 目录下:<br />
<br />
<pre><br />
├── abstract_message_port.h<br />
├── asio_message_port.cpp<br />
├── asio_message_port.h<br />
├── asio_ssl_context.cpp<br />
├── asio_ssl_context.h<br />
├── hostandport.cpp<br />
├── hostandport.h<br />
├── hostandport_test.cpp<br />
├── hostname_canonicalization.cpp<br />
├── hostname_canonicalization.h<br />
├── listen.cpp<br />
├── listen.h<br />
├── message.cpp<br />
├── message.h<br />
├── message_port.cpp<br />
├── message_port.h<br />
├── message_port_mock.cpp<br />
├── message_port_mock.h<br />
├── message_port_startup_param.cpp<br />
├── message_port_startup_param.h<br />
├── op_msg.cpp<br />
├── op_msg.h<br />
├── sock.cpp<br />
├── sock.h<br />
├── sock_test.cpp<br />
├── sockaddr.cpp<br />
├── sockaddr.h<br />
├── socket_exception.cpp<br />
├── socket_exception.h<br />
├── socket_poll.cpp<br />
├── socket_poll.h<br />
├── ssl_expiration.cpp<br />
├── ssl_expiration.h<br />
├── ssl_manager.cpp<br />
├── ssl_manager.h<br />
├── ssl_options.cpp<br />
├── ssl_options.h<br />
├── ssl_types.h<br />
├── thread_idle_callback.cpp<br />
└── thread_idle_callback.h<br />
</pre><br />
<br />
* 针对上层提供的服务接口定义在 src/mongo/transport 下,核心就是 transport_layer , transport_layer_legacy 和 service_entry_point,具体的关系是 TransportLayer 持有一个 acceptor ,当连接进来,包装成 session,然后调用ServiceEntryPoint.startSession 方法,进入一个读取请求-处理请求-应答请求的循环。 这里可能为了兼容老的代码,transport_layer_legacy 实现了 TransportLayer,并兼容老的代码。 ServiceEntryPoint 的子类 ServiceEntryPointImpl 里实现了 startSession 和 _sessionLoop 框架,每个连接启动一个线程处理:<br />
<br />
<pre><br />
//service_entry_point_impl.cpp<br />
void ServiceEntryPointImpl::startSession(transport::SessionHandle session) {<br />
// Pass ownership of the transport::SessionHandle into our worker thread. When this<br />
// thread exits, the session will end.<br />
launchWrappedServiceEntryWorkerThread(<br />
std::move(session), [this](const transport::SessionHandle& session) {<br />
_nWorkers.fetchAndAdd(1);<br />
auto guard = MakeGuard([&] { _nWorkers.fetchAndSubtract(1); });<br />
<br />
_sessionLoop(session);<br />
});<br />
}<br />
<br />
void ServiceEntryPointImpl::_sessionLoop(const transport::SessionHandle& session) {<br />
Message inMessage;<br />
bool inExhaust = false;<br />
int64_t counter = 0;<br />
<br />
while (true) {<br />
.........<br />
// The handleRequest is implemented in a subclass for mongod/mongos and actually all the<br />
// database work for this request.<br />
DbResponse dbresponse = this->handleRequest(opCtx.get(), inMessage, session->remote());<br />
......<br />
}<br />
}<br />
</pre><br />
<br />
实际是转交给 ServiceEntryPointImpl 的子类 mongod 和 mongos 的 handleRequest 处理请求。<br />
<br />
* transport_layer_legacy.cpp 就是调用 util/net 下面的类和方法,实现一个典型的 TCP 服务器了,其中 handleNewConnection 处理新建连接,构造函数里开始 listen:<br />
<br />
<pre><br />
TransportLayerLegacy::TransportLayerLegacy(const TransportLayerLegacy::Options& opts,<br />
ServiceEntryPoint* sep)<br />
: _sep(sep),<br />
_listener(stdx::make_unique<ListenerLegacy>(<br />
opts,<br />
stdx::bind(&TransportLayerLegacy::_handleNewConnection, this, stdx::placeholders::_1))),<br />
_running(false),<br />
_options(opts) {}<br />
<br />
void TransportLayerLegacy::_handleNewConnection(std::unique_ptr<AbstractMessagingPort> amp) {<br />
if (!Listener::globalTicketHolder.tryAcquire()) {<br />
log() << "connection refused because too many open connections: "<br />
<< Listener::globalTicketHolder.used();<br />
amp->shutdown();<br />
return;<br />
}<br />
<br />
amp->setLogLevel(logger::LogSeverity::Debug(1));<br />
auto session = LegacySession::create(std::move(amp), this);<br />
<br />
stdx::list<std::weak_ptr<LegacySession>> list;<br />
auto it = list.emplace(list.begin(), session);<br />
<br />
{<br />
// Add the new session to our list<br />
stdx::lock_guard<stdx::mutex> lk(_sessionsMutex);<br />
session->setIter(it);<br />
_sessions.splice(_sessions.begin(), list, it);<br />
}<br />
<br />
invariant(_sep);<br />
_sep->startSession(std::move(session));<br />
}<br />
<br />
</pre><br />
<br />
调用了 _sep->startSession(std::move(session));,其中 _sep 就是 ServiceEntryPoint 指针,在头文件 class 定义了。<br />
<br />
* 网络层来看, accept 采用了 select 调用,读写请求还是同步阻塞的过程。</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0
Mongodb 源码阅读笔记
2017-05-24T07:23:44Z
<p>Dennis zhuang:/* 网络层 */</p>
<hr />
<div><br />
== 网络层 ==<br />
<br />
* 具体实现在 src/mongo/util/net 目录下:<br />
<br />
<pre><br />
├── abstract_message_port.h<br />
├── asio_message_port.cpp<br />
├── asio_message_port.h<br />
├── asio_ssl_context.cpp<br />
├── asio_ssl_context.h<br />
├── hostandport.cpp<br />
├── hostandport.h<br />
├── hostandport_test.cpp<br />
├── hostname_canonicalization.cpp<br />
├── hostname_canonicalization.h<br />
├── listen.cpp<br />
├── listen.h<br />
├── message.cpp<br />
├── message.h<br />
├── message_port.cpp<br />
├── message_port.h<br />
├── message_port_mock.cpp<br />
├── message_port_mock.h<br />
├── message_port_startup_param.cpp<br />
├── message_port_startup_param.h<br />
├── op_msg.cpp<br />
├── op_msg.h<br />
├── sock.cpp<br />
├── sock.h<br />
├── sock_test.cpp<br />
├── sockaddr.cpp<br />
├── sockaddr.h<br />
├── socket_exception.cpp<br />
├── socket_exception.h<br />
├── socket_poll.cpp<br />
├── socket_poll.h<br />
├── ssl_expiration.cpp<br />
├── ssl_expiration.h<br />
├── ssl_manager.cpp<br />
├── ssl_manager.h<br />
├── ssl_options.cpp<br />
├── ssl_options.h<br />
├── ssl_types.h<br />
├── thread_idle_callback.cpp<br />
└── thread_idle_callback.h<br />
</pre><br />
<br />
* 针对上层提供的服务接口定义在 src/mongo/transport 下,核心就是 transport_layer , transport_layer_legacy 和 service_entry_point,具体的关系是 TransportLayer 持有一个 acceptor ,当连接进来,包装成 session,然后调用ServiceEntryPoint.startSession 方法,进入一个读取请求-处理请求-应答请求的循环。 这里可能为了兼容老的代码,transport_layer_legacy 实现了 TransportLayer,并兼容老的代码。 ServiceEntryPoint 的子类 ServiceEntryPointImpl 里实现了 startSession 和 _sessionLoop 框架,每个连接启动一个线程处理:<br />
<br />
<pre><br />
//service_entry_point_impl.cpp<br />
void ServiceEntryPointImpl::startSession(transport::SessionHandle session) {<br />
// Pass ownership of the transport::SessionHandle into our worker thread. When this<br />
// thread exits, the session will end.<br />
launchWrappedServiceEntryWorkerThread(<br />
std::move(session), [this](const transport::SessionHandle& session) {<br />
_nWorkers.fetchAndAdd(1);<br />
auto guard = MakeGuard([&] { _nWorkers.fetchAndSubtract(1); });<br />
<br />
_sessionLoop(session);<br />
});<br />
}<br />
<br />
void ServiceEntryPointImpl::_sessionLoop(const transport::SessionHandle& session) {<br />
Message inMessage;<br />
bool inExhaust = false;<br />
int64_t counter = 0;<br />
<br />
while (true) {<br />
...<br />
}<br />
}<br />
</pre></div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0
Mongodb 源码阅读笔记
2017-05-24T07:09:47Z
<p>Dennis zhuang:以“ == 网络层 == * 具体实现在 src/mongo/util/net 目录下: <pre> ├── abstract_message_port.h ├── asio_message_port.cpp ├── asio_message_por...”为内容创建页面</p>
<hr />
<div><br />
== 网络层 ==<br />
<br />
* 具体实现在 src/mongo/util/net 目录下:<br />
<br />
<pre><br />
├── abstract_message_port.h<br />
├── asio_message_port.cpp<br />
├── asio_message_port.h<br />
├── asio_ssl_context.cpp<br />
├── asio_ssl_context.h<br />
├── hostandport.cpp<br />
├── hostandport.h<br />
├── hostandport_test.cpp<br />
├── hostname_canonicalization.cpp<br />
├── hostname_canonicalization.h<br />
├── listen.cpp<br />
├── listen.h<br />
├── message.cpp<br />
├── message.h<br />
├── message_port.cpp<br />
├── message_port.h<br />
├── message_port_mock.cpp<br />
├── message_port_mock.h<br />
├── message_port_startup_param.cpp<br />
├── message_port_startup_param.h<br />
├── op_msg.cpp<br />
├── op_msg.h<br />
├── sock.cpp<br />
├── sock.h<br />
├── sock_test.cpp<br />
├── sockaddr.cpp<br />
├── sockaddr.h<br />
├── socket_exception.cpp<br />
├── socket_exception.h<br />
├── socket_poll.cpp<br />
├── socket_poll.h<br />
├── ssl_expiration.cpp<br />
├── ssl_expiration.h<br />
├── ssl_manager.cpp<br />
├── ssl_manager.h<br />
├── ssl_options.cpp<br />
├── ssl_options.h<br />
├── ssl_types.h<br />
├── thread_idle_callback.cpp<br />
└── thread_idle_callback.h<br />
</pre><br />
<br />
* 针对上层提供的服务接口定义在 src/mongo/transport 下,核心就是 transport_layer 和 service_entry_point:</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E7%AC%94%E8%AE%B0
Mongodb 存储引擎笔记
2017-05-23T07:28:12Z
<p>Dennis zhuang:</p>
<hr />
<div><br />
== 概述 ==<br />
<br />
* 同一个 replica set 可以使用不同的存储引擎。<br />
* 默认 wiredtiger 使用 snappy block compression;zlib 压缩算法,索引使用 prefix compression。<br />
* Starting in 3.4, the WiredTiger internal cache, by default, will use the larger of either: 50% of RAM minus 1 GB, or 256 MB.容器使用需要调整 storage.wiredTiger.engineConfig.cacheSizeGB<br />
* WT 引擎 60秒或者超过 2g 日志数据,写入一次 checkpoint<br />
* WT日志数据写入磁盘,从3.2开始是50毫秒间隔。<br />
* 文档大小统计, totalSize = indexSize + dataSize, 而 storageSize 是实际占用大小,包括未使用的空间。<br />
<br />
=== WT 引擎 ===<br />
<br />
* 文档级别锁,部分操作如删除 collection 仍然需要 db 级别的独占锁,以及一些跨多个数据库的操作,需要 instance 级别锁。<br />
* MVCC 隔离。<br />
* checkpoint 机制,每个 60 秒创建一个检查点,加快恢复,哪怕没有启用日志。<br />
* uses write-ahead transaction log in combination with checkpoints to ensure data durability<br />
* 如果有使用 replica set,理论上 journal 日志是可以关掉的。<br />
<br />
<br />
==== WT 引擎配置项目 ====<br />
<br />
<code><br />
storage:<br />
wiredTiger:<br />
engineConfig:<br />
cacheSizeGB: 内部数据缓存大小,从 3.4 开始,是 50% 可用内存减去 1G,或者 256MB。<br />
journalCompressor: 日志压缩,默认压缩算法 snappy,可以选择 zlib。<br />
directoryForIndexes: 默认为 false,如果为 true,将数据和索引会分成两个目录 index/collection 存储。<br />
collectionConfig:<br />
blockCompressor: 数据压缩算法,默认也是 snappy<br />
indexConfig:<br />
prefixCompression: 默认为 true,对索引使用前缀压缩。可以节省内存和磁盘占用。<br />
</code><br />
<br />
== MMAPv1 引擎 ==<br />
* 使用 mmap() 将文件映射到内存。<br />
* 写数据是 60 秒,写日志是间隔 100 毫秒,通过 storage.syncPeriodSecs 和 storage.journal.commitIntervalMs 修改。<br />
* 文件比数据更大,因为:<br />
** 预分配文件,可以通过 storage.mmapv1.smallFiles 来减少初始文件大小,并限制最大 512m<br />
** oplog.rs 文件,也是预先创建的 capped collection,默认占用 5% 的磁盘空间(64位安装),通常不建议改这个。<br />
** 日志文件<br />
** 空记录,已删除的 collection 和文档都会保留在文件里。mongodb 会复用这些空间的,但是不会归还给 os。为了更有效的使用这些空间,你可以减少碎片,使用 compact 命令。compact 需要额外的 2g 磁盘空间来运行。compact 只是减少文件碎片,但是也不会归还空间给 os,想要归还可以用 repairDatabase 命令。repairDatabase 需要更大的磁盘空间来运行——等于你当前的数据集合大小,加上 2g。归还也可以通过重新同步来,特别是 secondary 成员。<br />
* page faults,发生在写入或者读取,当要写或者读的部分不在内存的时候发生,通常是因为内存不足引起的。分成 soft 和 hard, hard 读磁盘, soft 只是在 os 的文件缓存里移动。<br />
* 在更新的时候,如果文档增大, 文档可能需要在 disk 搬迁,为了减少这种影响, mongodb 使用 padding 对齐技术。对齐是自动的,也不建议手动。<br />
* 3.0 开始,MMAPv1 用的是 power of 2 大小做对齐,也可以在创建 collection 或通过 collMod 命令修改为 noPadding 策略。<br />
<br />
=== MMAPv1 配置项目 ===<br />
<br />
<code><br />
<br />
storage:<br />
mmapv1:<br />
preallocDataFiles: 默认false,是否预分配文件<br />
nsSize: 默认16mb, namespace 文件(.ns结尾)的默认大小,每个collection和index作为一个 namespace, 16MB 可以容纳 24000 个 namespace。<br />
quota:<br />
enforced: 默认false,是否限制单个 db 的最大数据文件数目,如果设置了,最大为8个个文件,可以通过 maxFilesPerDB 修改<br />
maxFilesPerDB: 默认8,见 enforced<br />
smallFiles: 默认false,如果设置为 true,将减少数据文件初始大小,并限制最大为 512m,同时减少日志文件从 1g 到 128m。<br />
journal:<br />
debugFlags: 测试 debug flags<br />
commitIntervalMs: 被 storage.journal.commitIntervalMS 替代,日志落盘间隔,默认是 100ms。<br />
</code><br />
<br />
== 锁 ==<br />
<br />
* intent lock:意图锁,参见 https://stackoverflow.com/questions/34613068/what-are-intention-shared-and-intention-exclusive-locks-in-mongodb 和 https://en.wikipedia.org/wiki/Multiple_granularity_locking,按照我的理解就是将通过高层意图锁,来避免底层次锁的无谓争用。<br />
* MMAP 是 collection 级别,而 WT 已经是 document 级别。<br />
* 锁会退让,如果预测读取的数据不在内存,会让出读锁,加载到内存,再重新获取锁。写如果涉及多个文档,也可能退让锁。<br />
* 锁和操作,见图<br />
[[文件:Mongodb lock op.png]]</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=%E6%96%87%E4%BB%B6:Mongodb_lock_op.png
文件:Mongodb lock op.png
2017-05-23T07:27:53Z
<p>Dennis zhuang:</p>
<hr />
<div></div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E7%AC%94%E8%AE%B0
Mongodb 存储引擎笔记
2017-05-23T06:56:47Z
<p>Dennis zhuang:/* WT 引擎 */</p>
<hr />
<div><br />
== 概述 ==<br />
<br />
* 同一个 replica set 可以使用不同的存储引擎。<br />
* 默认 wiredtiger 使用 snappy block compression;zlib 压缩算法,索引使用 prefix compression。<br />
* Starting in 3.4, the WiredTiger internal cache, by default, will use the larger of either: 50% of RAM minus 1 GB, or 256 MB.容器使用需要调整 storage.wiredTiger.engineConfig.cacheSizeGB<br />
* WT 引擎 60秒或者超过 2g 日志数据,写入一次 checkpoint<br />
* WT日志数据写入磁盘,从3.2开始是50毫秒间隔。<br />
* 文档大小统计, totalSize = indexSize + dataSize, 而 storageSize 是实际占用大小,包括未使用的空间。<br />
<br />
=== WT 引擎 ===<br />
<br />
* 文档级别锁,部分操作如删除 collection 仍然需要 db 级别的独占锁,以及一些跨多个数据库的操作,需要 instance 级别锁。<br />
* MVCC 隔离。<br />
* checkpoint 机制,每个 60 秒创建一个检查点,加快恢复,哪怕没有启用日志。<br />
* uses write-ahead transaction log in combination with checkpoints to ensure data durability<br />
* 如果有使用 replica set,理论上 journal 日志是可以关掉的。<br />
<br />
<br />
==== WT 引擎配置项目 ====<br />
<br />
<code><br />
storage:<br />
wiredTiger:<br />
engineConfig:<br />
cacheSizeGB: 内部数据缓存大小,从 3.4 开始,是 50% 可用内存减去 1G,或者 256MB。<br />
journalCompressor: 日志压缩,默认压缩算法 snappy,可以选择 zlib。<br />
directoryForIndexes: 默认为 false,如果为 true,将数据和索引会分成两个目录 index/collection 存储。<br />
collectionConfig:<br />
blockCompressor: 数据压缩算法,默认也是 snappy<br />
indexConfig:<br />
prefixCompression: 默认为 true,对索引使用前缀压缩。可以节省内存和磁盘占用。<br />
</code><br />
<br />
== MMAPv1 引擎 ==<br />
* 使用 mmap() 将文件映射到内存。<br />
* 写数据是 60 秒,写日志是间隔 100 毫秒,通过 storage.syncPeriodSecs 和 storage.journal.commitIntervalMs 修改。<br />
* 文件比数据更大,因为:<br />
** 预分配文件,可以通过 storage.mmapv1.smallFiles 来减少初始文件大小,并限制最大 512m<br />
** oplog.rs 文件,也是预先创建的 capped collection,默认占用 5% 的磁盘空间(64位安装),通常不建议改这个。<br />
** 日志文件<br />
** 空记录,已删除的 collection 和文档都会保留在文件里。mongodb 会复用这些空间的,但是不会归还给 os。为了更有效的使用这些空间,你可以减少碎片,使用 compact 命令。compact 需要额外的 2g 磁盘空间来运行。compact 只是减少文件碎片,但是也不会归还空间给 os,想要归还可以用 repairDatabase 命令。repairDatabase 需要更大的磁盘空间来运行——等于你当前的数据集合大小,加上 2g。归还也可以通过重新同步来,特别是 secondary 成员。<br />
* page faults,发生在写入或者读取,当要写或者读的部分不在内存的时候发生,通常是因为内存不足引起的。分成 soft 和 hard, hard 读磁盘, soft 只是在 os 的文件缓存里移动。<br />
* 在更新的时候,如果文档增大, 文档可能需要在 disk 搬迁,为了减少这种影响, mongodb 使用 padding 对齐技术。对齐是自动的,也不建议手动。<br />
* 3.0 开始,MMAPv1 用的是 power of 2 大小做对齐,也可以在创建 collection 或通过 collMod 命令修改为 noPadding 策略。<br />
<br />
=== MMAPv1 配置项目 ===<br />
<br />
<code><br />
<br />
storage:<br />
mmapv1:<br />
preallocDataFiles: 默认false,是否预分配文件<br />
nsSize: 默认16mb, namespace 文件(.ns结尾)的默认大小,每个collection和index作为一个 namespace, 16MB 可以容纳 24000 个 namespace。<br />
quota:<br />
enforced: 默认false,是否限制单个 db 的最大数据文件数目,如果设置了,最大为8个个文件,可以通过 maxFilesPerDB 修改<br />
maxFilesPerDB: 默认8,见 enforced<br />
smallFiles: 默认false,如果设置为 true,将减少数据文件初始大小,并限制最大为 512m,同时减少日志文件从 1g 到 128m。<br />
journal:<br />
debugFlags: 测试 debug flags<br />
commitIntervalMs: 被 storage.journal.commitIntervalMS 替代,日志落盘间隔,默认是 100ms。<br />
</code></div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E7%AC%94%E8%AE%B0
Mongodb 存储引擎笔记
2017-05-23T06:56:19Z
<p>Dennis zhuang:/* 概述 */</p>
<hr />
<div><br />
== 概述 ==<br />
<br />
* 同一个 replica set 可以使用不同的存储引擎。<br />
* 默认 wiredtiger 使用 snappy block compression;zlib 压缩算法,索引使用 prefix compression。<br />
* Starting in 3.4, the WiredTiger internal cache, by default, will use the larger of either: 50% of RAM minus 1 GB, or 256 MB.容器使用需要调整 storage.wiredTiger.engineConfig.cacheSizeGB<br />
* WT 引擎 60秒或者超过 2g 日志数据,写入一次 checkpoint<br />
* WT日志数据写入磁盘,从3.2开始是50毫秒间隔。<br />
* 文档大小统计, totalSize = indexSize + dataSize, 而 storageSize 是实际占用大小,包括未使用的空间。<br />
<br />
=== WT 引擎 ===<br />
<br />
* 文档级别锁,部分操作如删除 collection 仍然需要 db 级别的独占锁,以及一些跨多个数据库的操作,需要 instance 级别锁。<br />
* MVCC 隔离。<br />
* checkpoint 机制,每个 60 秒创建一个检查点,加快恢复,哪怕没有启用日志。<br />
* uses write-ahead transaction log in combination with checkpoints to ensure data durability<br />
<br />
<br />
==== WT 引擎配置项目 ====<br />
<br />
<code><br />
storage:<br />
wiredTiger:<br />
engineConfig:<br />
cacheSizeGB: 内部数据缓存大小,从 3.4 开始,是 50% 可用内存减去 1G,或者 256MB。<br />
journalCompressor: 日志压缩,默认压缩算法 snappy,可以选择 zlib。<br />
directoryForIndexes: 默认为 false,如果为 true,将数据和索引会分成两个目录 index/collection 存储。<br />
collectionConfig:<br />
blockCompressor: 数据压缩算法,默认也是 snappy<br />
indexConfig:<br />
prefixCompression: 默认为 true,对索引使用前缀压缩。可以节省内存和磁盘占用。<br />
</code><br />
<br />
== MMAPv1 引擎 ==<br />
* 使用 mmap() 将文件映射到内存。<br />
* 写数据是 60 秒,写日志是间隔 100 毫秒,通过 storage.syncPeriodSecs 和 storage.journal.commitIntervalMs 修改。<br />
* 文件比数据更大,因为:<br />
** 预分配文件,可以通过 storage.mmapv1.smallFiles 来减少初始文件大小,并限制最大 512m<br />
** oplog.rs 文件,也是预先创建的 capped collection,默认占用 5% 的磁盘空间(64位安装),通常不建议改这个。<br />
** 日志文件<br />
** 空记录,已删除的 collection 和文档都会保留在文件里。mongodb 会复用这些空间的,但是不会归还给 os。为了更有效的使用这些空间,你可以减少碎片,使用 compact 命令。compact 需要额外的 2g 磁盘空间来运行。compact 只是减少文件碎片,但是也不会归还空间给 os,想要归还可以用 repairDatabase 命令。repairDatabase 需要更大的磁盘空间来运行——等于你当前的数据集合大小,加上 2g。归还也可以通过重新同步来,特别是 secondary 成员。<br />
* page faults,发生在写入或者读取,当要写或者读的部分不在内存的时候发生,通常是因为内存不足引起的。分成 soft 和 hard, hard 读磁盘, soft 只是在 os 的文件缓存里移动。<br />
* 在更新的时候,如果文档增大, 文档可能需要在 disk 搬迁,为了减少这种影响, mongodb 使用 padding 对齐技术。对齐是自动的,也不建议手动。<br />
* 3.0 开始,MMAPv1 用的是 power of 2 大小做对齐,也可以在创建 collection 或通过 collMod 命令修改为 noPadding 策略。<br />
<br />
=== MMAPv1 配置项目 ===<br />
<br />
<code><br />
<br />
storage:<br />
mmapv1:<br />
preallocDataFiles: 默认false,是否预分配文件<br />
nsSize: 默认16mb, namespace 文件(.ns结尾)的默认大小,每个collection和index作为一个 namespace, 16MB 可以容纳 24000 个 namespace。<br />
quota:<br />
enforced: 默认false,是否限制单个 db 的最大数据文件数目,如果设置了,最大为8个个文件,可以通过 maxFilesPerDB 修改<br />
maxFilesPerDB: 默认8,见 enforced<br />
smallFiles: 默认false,如果设置为 true,将减少数据文件初始大小,并限制最大为 512m,同时减少日志文件从 1g 到 128m。<br />
journal:<br />
debugFlags: 测试 debug flags<br />
commitIntervalMs: 被 storage.journal.commitIntervalMS 替代,日志落盘间隔,默认是 100ms。<br />
</code></div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E7%AC%94%E8%AE%B0
Mongodb 存储引擎笔记
2017-05-23T06:45:57Z
<p>Dennis zhuang:/* MMAPv1 引擎 */</p>
<hr />
<div><br />
== 概述 ==<br />
<br />
* 同一个 replica set 可以使用不同的存储引擎。<br />
* 默认 wiredtiger 使用 snappy block compression;zlib 压缩算法,索引使用 prefix compression。<br />
* Starting in 3.4, the WiredTiger internal cache, by default, will use the larger of either: 50% of RAM minus 1 GB, or 256 MB.容器使用需要调整 storage.wiredTiger.engineConfig.cacheSizeGB<br />
* WT 引擎 60秒或者超过 2g 日志数据,写入一次 checkpoint<br />
* WT日志数据写入磁盘,从3.2开始是50毫秒间隔。<br />
* 文档大小统计, totalSize = indexSize + dataSize, 而 storageSize 是实际占用大小,包括未使用的空间。<br />
<br />
=== WT 引擎配置项目 ===<br />
<br />
<code><br />
storage:<br />
wiredTiger:<br />
engineConfig:<br />
cacheSizeGB: 内部数据缓存大小,从 3.4 开始,是 50% 可用内存减去 1G,或者 256MB。<br />
journalCompressor: 日志压缩,默认压缩算法 snappy,可以选择 zlib。<br />
directoryForIndexes: 默认为 false,如果为 true,将数据和索引会分成两个目录 index/collection 存储。<br />
collectionConfig:<br />
blockCompressor: 数据压缩算法,默认也是 snappy<br />
indexConfig:<br />
prefixCompression: 默认为 true,对索引使用前缀压缩。可以节省内存和磁盘占用。<br />
</code><br />
<br />
== MMAPv1 引擎 ==<br />
* 使用 mmap() 将文件映射到内存。<br />
* 写数据是 60 秒,写日志是间隔 100 毫秒,通过 storage.syncPeriodSecs 和 storage.journal.commitIntervalMs 修改。<br />
* 文件比数据更大,因为:<br />
** 预分配文件,可以通过 storage.mmapv1.smallFiles 来减少初始文件大小,并限制最大 512m<br />
** oplog.rs 文件,也是预先创建的 capped collection,默认占用 5% 的磁盘空间(64位安装),通常不建议改这个。<br />
** 日志文件<br />
** 空记录,已删除的 collection 和文档都会保留在文件里。mongodb 会复用这些空间的,但是不会归还给 os。为了更有效的使用这些空间,你可以减少碎片,使用 compact 命令。compact 需要额外的 2g 磁盘空间来运行。compact 只是减少文件碎片,但是也不会归还空间给 os,想要归还可以用 repairDatabase 命令。repairDatabase 需要更大的磁盘空间来运行——等于你当前的数据集合大小,加上 2g。归还也可以通过重新同步来,特别是 secondary 成员。<br />
* page faults,发生在写入或者读取,当要写或者读的部分不在内存的时候发生,通常是因为内存不足引起的。分成 soft 和 hard, hard 读磁盘, soft 只是在 os 的文件缓存里移动。<br />
* 在更新的时候,如果文档增大, 文档可能需要在 disk 搬迁,为了减少这种影响, mongodb 使用 padding 对齐技术。对齐是自动的,也不建议手动。<br />
* 3.0 开始,MMAPv1 用的是 power of 2 大小做对齐,也可以在创建 collection 或通过 collMod 命令修改为 noPadding 策略。<br />
<br />
=== MMAPv1 配置项目 ===<br />
<br />
<code><br />
<br />
storage:<br />
mmapv1:<br />
preallocDataFiles: 默认false,是否预分配文件<br />
nsSize: 默认16mb, namespace 文件(.ns结尾)的默认大小,每个collection和index作为一个 namespace, 16MB 可以容纳 24000 个 namespace。<br />
quota:<br />
enforced: 默认false,是否限制单个 db 的最大数据文件数目,如果设置了,最大为8个个文件,可以通过 maxFilesPerDB 修改<br />
maxFilesPerDB: 默认8,见 enforced<br />
smallFiles: 默认false,如果设置为 true,将减少数据文件初始大小,并限制最大为 512m,同时减少日志文件从 1g 到 128m。<br />
journal:<br />
debugFlags: 测试 debug flags<br />
commitIntervalMs: 被 storage.journal.commitIntervalMS 替代,日志落盘间隔,默认是 100ms。<br />
</code></div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E7%AC%94%E8%AE%B0
Mongodb 存储引擎笔记
2017-05-23T03:53:15Z
<p>Dennis zhuang:/* WT 引擎配置项目 */</p>
<hr />
<div><br />
== 概述 ==<br />
<br />
* 同一个 replica set 可以使用不同的存储引擎。<br />
* 默认 wiredtiger 使用 snappy block compression;zlib 压缩算法,索引使用 prefix compression。<br />
* Starting in 3.4, the WiredTiger internal cache, by default, will use the larger of either: 50% of RAM minus 1 GB, or 256 MB.容器使用需要调整 storage.wiredTiger.engineConfig.cacheSizeGB<br />
* WT 引擎 60秒或者超过 2g 日志数据,写入一次 checkpoint<br />
* WT日志数据写入磁盘,从3.2开始是50毫秒间隔。<br />
* 文档大小统计, totalSize = indexSize + dataSize, 而 storageSize 是实际占用大小,包括未使用的空间。<br />
<br />
=== WT 引擎配置项目 ===<br />
<br />
<code><br />
storage:<br />
wiredTiger:<br />
engineConfig:<br />
cacheSizeGB: 内部数据缓存大小,从 3.4 开始,是 50% 可用内存减去 1G,或者 256MB。<br />
journalCompressor: 日志压缩,默认压缩算法 snappy,可以选择 zlib。<br />
directoryForIndexes: 默认为 false,如果为 true,将数据和索引会分成两个目录 index/collection 存储。<br />
collectionConfig:<br />
blockCompressor: 数据压缩算法,默认也是 snappy<br />
indexConfig:<br />
prefixCompression: 默认为 true,对索引使用前缀压缩。可以节省内存和磁盘占用。<br />
</code><br />
<br />
== MMAPv1 引擎 ==<br />
* 使用 mmap() 将文件映射到内存。<br />
* 写数据是 60 秒,写日志是间隔 100 毫秒,通过 storage.syncPeriodSecs 和 storage.journal.commitIntervalMs 修改。<br />
* 文件比数据更大,因为:<br />
** 预分配文件,可以通过 storage.mmapv1.smallFiles 来减少初始文件大小,并限制最大 512m<br />
** oplog.rs 文件,也是预先创建的 capped collection,默认占用 5% 的磁盘空间(64位安装),通常不建议改这个。<br />
** 日志文件<br />
** 空记录,已删除的 collection 和文档都会保留在文件里。mongodb 会复用这些空间的,但是不会归还给 os。为了更有效的使用这些空间,你可以减少碎片,使用 compact 命令。compact 需要额外的 2g 磁盘空间来运行。compact 只是减少文件碎片,但是也不会归还空间给 os,想要归还可以用 repairDatabase 命令。repairDatabase 需要更大的磁盘空间来运行——等于你当前的数据集合大小,加上 2g。归还也可以通过重新同步来,特别是 secondary 成员。<br />
* page faults,发生在写入或者读取,当要写或者读的部分不在内存的时候发生,通常是因为内存不足引起的。分成 soft 和 hard, hard 读磁盘, soft 只是在 os 的文件缓存里移动。<br />
* 在更新的时候,如果文档增大, 文档可能需要在 disk 搬迁,为了减少这种影响, mongodb 使用 padding 对齐技术。对齐是自动的,也不建议手动。<br />
<br />
=== MMAPv1 配置项目 ===<br />
<br />
<code><br />
<br />
storage:<br />
mmapv1:<br />
preallocDataFiles: 默认false,是否预分配文件<br />
nsSize: 默认16mb, namespace 文件(.ns结尾)的默认大小,每个collection和index作为一个 namespace, 16MB 可以容纳 24000 个 namespace。<br />
quota:<br />
enforced: 默认false,是否限制单个 db 的最大数据文件数目,如果设置了,最大为8个个文件,可以通过 maxFilesPerDB 修改<br />
maxFilesPerDB: 默认8,见 enforced<br />
smallFiles: 默认false,如果设置为 true,将减少数据文件初始大小,并限制最大为 512m,同时减少日志文件从 1g 到 128m。<br />
journal:<br />
debugFlags: 测试 debug flags<br />
commitIntervalMs: 被 storage.journal.commitIntervalMS 替代,日志落盘间隔,默认是 100ms。<br />
</code></div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E7%AC%94%E8%AE%B0
Mongodb 存储引擎笔记
2017-05-23T03:49:05Z
<p>Dennis zhuang:/* 概述 */</p>
<hr />
<div><br />
== 概述 ==<br />
<br />
* 同一个 replica set 可以使用不同的存储引擎。<br />
* 默认 wiredtiger 使用 snappy block compression;zlib 压缩算法,索引使用 prefix compression。<br />
* Starting in 3.4, the WiredTiger internal cache, by default, will use the larger of either: 50% of RAM minus 1 GB, or 256 MB.容器使用需要调整 storage.wiredTiger.engineConfig.cacheSizeGB<br />
* WT 引擎 60秒或者超过 2g 日志数据,写入一次 checkpoint<br />
* WT日志数据写入磁盘,从3.2开始是50毫秒间隔。<br />
* 文档大小统计, totalSize = indexSize + dataSize, 而 storageSize 是实际占用大小,包括未使用的空间。<br />
<br />
=== WT 引擎配置项目 ===<br />
<br />
<code><br />
storage:<br />
wiredTiger:<br />
engineConfig:<br />
cacheSizeGB: <number><br />
journalCompressor: <string><br />
directoryForIndexes: <boolean><br />
collectionConfig:<br />
blockCompressor: <string><br />
indexConfig:<br />
prefixCompression: <boolean><br />
</code><br />
<br />
== MMAPv1 引擎 ==<br />
* 使用 mmap() 将文件映射到内存。<br />
* 写数据是 60 秒,写日志是间隔 100 毫秒,通过 storage.syncPeriodSecs 和 storage.journal.commitIntervalMs 修改。<br />
* 文件比数据更大,因为:<br />
** 预分配文件,可以通过 storage.mmapv1.smallFiles 来减少初始文件大小,并限制最大 512m<br />
** oplog.rs 文件,也是预先创建的 capped collection,默认占用 5% 的磁盘空间(64位安装),通常不建议改这个。<br />
** 日志文件<br />
** 空记录,已删除的 collection 和文档都会保留在文件里。mongodb 会复用这些空间的,但是不会归还给 os。为了更有效的使用这些空间,你可以减少碎片,使用 compact 命令。compact 需要额外的 2g 磁盘空间来运行。compact 只是减少文件碎片,但是也不会归还空间给 os,想要归还可以用 repairDatabase 命令。repairDatabase 需要更大的磁盘空间来运行——等于你当前的数据集合大小,加上 2g。归还也可以通过重新同步来,特别是 secondary 成员。<br />
* page faults,发生在写入或者读取,当要写或者读的部分不在内存的时候发生,通常是因为内存不足引起的。分成 soft 和 hard, hard 读磁盘, soft 只是在 os 的文件缓存里移动。<br />
* 在更新的时候,如果文档增大, 文档可能需要在 disk 搬迁,为了减少这种影响, mongodb 使用 padding 对齐技术。对齐是自动的,也不建议手动。<br />
<br />
=== MMAPv1 配置项目 ===<br />
<br />
<code><br />
<br />
storage:<br />
mmapv1:<br />
preallocDataFiles: 默认false,是否预分配文件<br />
nsSize: 默认16mb, namespace 文件(.ns结尾)的默认大小,每个collection和index作为一个 namespace, 16MB 可以容纳 24000 个 namespace。<br />
quota:<br />
enforced: 默认false,是否限制单个 db 的最大数据文件数目,如果设置了,最大为8个个文件,可以通过 maxFilesPerDB 修改<br />
maxFilesPerDB: 默认8,见 enforced<br />
smallFiles: 默认false,如果设置为 true,将减少数据文件初始大小,并限制最大为 512m,同时减少日志文件从 1g 到 128m。<br />
journal:<br />
debugFlags: 测试 debug flags<br />
commitIntervalMs: 被 storage.journal.commitIntervalMS 替代,日志落盘间隔,默认是 100ms。<br />
</code></div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E7%AC%94%E8%AE%B0
Mongodb 存储引擎笔记
2017-05-23T03:31:30Z
<p>Dennis zhuang:/* MMAPv1 配置项目 */</p>
<hr />
<div><br />
== 概述 ==<br />
<br />
* 同一个 replica set 可以使用不同的存储引擎。<br />
* 默认 wiredtiger 使用 snappy block compression;zlib 压缩算法,索引使用 prefix compression。<br />
* Starting in 3.4, the WiredTiger internal cache, by default, will use the larger of either: 50% of RAM minus 1 GB, or 256 MB.容器使用需要调整 storage.wiredTiger.engineConfig.cacheSizeGB<br />
* WT 引擎 60秒或者超过 2g 日志数据,写入一次 checkpoint<br />
* WT日志数据写入磁盘,从3.2开始是50毫秒间隔。<br />
* 文档大小统计, totalSize = indexSize + dataSize, 而 storageSize 是实际占用大小,包括未使用的空间。<br />
<br />
== MMAPv1 引擎 ==<br />
* 使用 mmap() 将文件映射到内存。<br />
* 写数据是 60 秒,写日志是间隔 100 毫秒,通过 storage.syncPeriodSecs 和 storage.journal.commitIntervalMs 修改。<br />
* 文件比数据更大,因为:<br />
** 预分配文件,可以通过 storage.mmapv1.smallFiles 来减少初始文件大小,并限制最大 512m<br />
** oplog.rs 文件,也是预先创建的 capped collection,默认占用 5% 的磁盘空间(64位安装),通常不建议改这个。<br />
** 日志文件<br />
** 空记录,已删除的 collection 和文档都会保留在文件里。mongodb 会复用这些空间的,但是不会归还给 os。为了更有效的使用这些空间,你可以减少碎片,使用 compact 命令。compact 需要额外的 2g 磁盘空间来运行。compact 只是减少文件碎片,但是也不会归还空间给 os,想要归还可以用 repairDatabase 命令。repairDatabase 需要更大的磁盘空间来运行——等于你当前的数据集合大小,加上 2g。归还也可以通过重新同步来,特别是 secondary 成员。<br />
* page faults,发生在写入或者读取,当要写或者读的部分不在内存的时候发生,通常是因为内存不足引起的。分成 soft 和 hard, hard 读磁盘, soft 只是在 os 的文件缓存里移动。<br />
* 在更新的时候,如果文档增大, 文档可能需要在 disk 搬迁,为了减少这种影响, mongodb 使用 padding 对齐技术。对齐是自动的,也不建议手动。<br />
<br />
=== MMAPv1 配置项目 ===<br />
<br />
<code><br />
<br />
storage:<br />
mmapv1:<br />
preallocDataFiles: 默认false,是否预分配文件<br />
nsSize: 默认16mb, namespace 文件(.ns结尾)的默认大小,每个collection和index作为一个 namespace, 16MB 可以容纳 24000 个 namespace。<br />
quota:<br />
enforced: 默认false,是否限制单个 db 的最大数据文件数目,如果设置了,最大为8个个文件,可以通过 maxFilesPerDB 修改<br />
maxFilesPerDB: 默认8,见 enforced<br />
smallFiles: 默认false,如果设置为 true,将减少数据文件初始大小,并限制最大为 512m,同时减少日志文件从 1g 到 128m。<br />
journal:<br />
debugFlags: 测试 debug flags<br />
commitIntervalMs: 被 storage.journal.commitIntervalMS 替代,日志落盘间隔,默认是 100ms。<br />
</code></div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E7%AC%94%E8%AE%B0
Mongodb 存储引擎笔记
2017-05-23T03:31:18Z
<p>Dennis zhuang:/* MMAPv1 配置项目 */</p>
<hr />
<div><br />
== 概述 ==<br />
<br />
* 同一个 replica set 可以使用不同的存储引擎。<br />
* 默认 wiredtiger 使用 snappy block compression;zlib 压缩算法,索引使用 prefix compression。<br />
* Starting in 3.4, the WiredTiger internal cache, by default, will use the larger of either: 50% of RAM minus 1 GB, or 256 MB.容器使用需要调整 storage.wiredTiger.engineConfig.cacheSizeGB<br />
* WT 引擎 60秒或者超过 2g 日志数据,写入一次 checkpoint<br />
* WT日志数据写入磁盘,从3.2开始是50毫秒间隔。<br />
* 文档大小统计, totalSize = indexSize + dataSize, 而 storageSize 是实际占用大小,包括未使用的空间。<br />
<br />
== MMAPv1 引擎 ==<br />
* 使用 mmap() 将文件映射到内存。<br />
* 写数据是 60 秒,写日志是间隔 100 毫秒,通过 storage.syncPeriodSecs 和 storage.journal.commitIntervalMs 修改。<br />
* 文件比数据更大,因为:<br />
** 预分配文件,可以通过 storage.mmapv1.smallFiles 来减少初始文件大小,并限制最大 512m<br />
** oplog.rs 文件,也是预先创建的 capped collection,默认占用 5% 的磁盘空间(64位安装),通常不建议改这个。<br />
** 日志文件<br />
** 空记录,已删除的 collection 和文档都会保留在文件里。mongodb 会复用这些空间的,但是不会归还给 os。为了更有效的使用这些空间,你可以减少碎片,使用 compact 命令。compact 需要额外的 2g 磁盘空间来运行。compact 只是减少文件碎片,但是也不会归还空间给 os,想要归还可以用 repairDatabase 命令。repairDatabase 需要更大的磁盘空间来运行——等于你当前的数据集合大小,加上 2g。归还也可以通过重新同步来,特别是 secondary 成员。<br />
* page faults,发生在写入或者读取,当要写或者读的部分不在内存的时候发生,通常是因为内存不足引起的。分成 soft 和 hard, hard 读磁盘, soft 只是在 os 的文件缓存里移动。<br />
* 在更新的时候,如果文档增大, 文档可能需要在 disk 搬迁,为了减少这种影响, mongodb 使用 padding 对齐技术。对齐是自动的,也不建议手动。<br />
<br />
=== MMAPv1 配置项目 ===<br />
<br />
<code><br />
<br />
storage:<br />
mmapv1:<br />
preallocDataFiles: 默认false,是否预分配文件<br />
nsSize: 默认16mb, namespace 文件(.ns结尾)的默认大小,每个collection和index作为一个 namespace, 16MB 可以容纳 24000 个 namespace。<br />
quota:<br />
enforced: 默认false,是否限制单个 db 的最大数据文件数目,如果设置了,最大为8个个文件,可以通过 maxFilesPerDB 修改<br />
maxFilesPerDB: 默认8,见 enforced<br />
smallFiles: 默认false,如果设置为 true,将减少数据文件初始大小,并限制最大为 512m,同时减少日志文件从 1g 到 128m。<br />
journal:<br />
debugFlags: 测试 debug flags<br />
commitIntervalMs: 被 storage.journal.commitIntervalMS 替代,日志落盘间隔,默认是 100ms。<br />
</code></div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E7%AC%94%E8%AE%B0
Mongodb 存储引擎笔记
2017-05-23T03:25:37Z
<p>Dennis zhuang:/* MMAPv1 引擎 */</p>
<hr />
<div><br />
== 概述 ==<br />
<br />
* 同一个 replica set 可以使用不同的存储引擎。<br />
* 默认 wiredtiger 使用 snappy block compression;zlib 压缩算法,索引使用 prefix compression。<br />
* Starting in 3.4, the WiredTiger internal cache, by default, will use the larger of either: 50% of RAM minus 1 GB, or 256 MB.容器使用需要调整 storage.wiredTiger.engineConfig.cacheSizeGB<br />
* WT 引擎 60秒或者超过 2g 日志数据,写入一次 checkpoint<br />
* WT日志数据写入磁盘,从3.2开始是50毫秒间隔。<br />
* 文档大小统计, totalSize = indexSize + dataSize, 而 storageSize 是实际占用大小,包括未使用的空间。<br />
<br />
== MMAPv1 引擎 ==<br />
* 使用 mmap() 将文件映射到内存。<br />
* 写数据是 60 秒,写日志是间隔 100 毫秒,通过 storage.syncPeriodSecs 和 storage.journal.commitIntervalMs 修改。<br />
* 文件比数据更大,因为:<br />
** 预分配文件,可以通过 storage.mmapv1.smallFiles 来减少初始文件大小,并限制最大 512m<br />
** oplog.rs 文件,也是预先创建的 capped collection,默认占用 5% 的磁盘空间(64位安装),通常不建议改这个。<br />
** 日志文件<br />
** 空记录,已删除的 collection 和文档都会保留在文件里。mongodb 会复用这些空间的,但是不会归还给 os。为了更有效的使用这些空间,你可以减少碎片,使用 compact 命令。compact 需要额外的 2g 磁盘空间来运行。compact 只是减少文件碎片,但是也不会归还空间给 os,想要归还可以用 repairDatabase 命令。repairDatabase 需要更大的磁盘空间来运行——等于你当前的数据集合大小,加上 2g。归还也可以通过重新同步来,特别是 secondary 成员。<br />
* page faults,发生在写入或者读取,当要写或者读的部分不在内存的时候发生,通常是因为内存不足引起的。分成 soft 和 hard, hard 读磁盘, soft 只是在 os 的文件缓存里移动。<br />
* 在更新的时候,如果文档增大, 文档可能需要在 disk 搬迁,为了减少这种影响, mongodb 使用 padding 对齐技术。对齐是自动的,也不建议手动。<br />
<br />
=== MMAPv1 配置项目 ===</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E7%AC%94%E8%AE%B0
Mongodb 存储引擎笔记
2017-05-23T03:25:17Z
<p>Dennis zhuang:/* 概述 */</p>
<hr />
<div><br />
== 概述 ==<br />
<br />
* 同一个 replica set 可以使用不同的存储引擎。<br />
* 默认 wiredtiger 使用 snappy block compression;zlib 压缩算法,索引使用 prefix compression。<br />
* Starting in 3.4, the WiredTiger internal cache, by default, will use the larger of either: 50% of RAM minus 1 GB, or 256 MB.容器使用需要调整 storage.wiredTiger.engineConfig.cacheSizeGB<br />
* WT 引擎 60秒或者超过 2g 日志数据,写入一次 checkpoint<br />
* WT日志数据写入磁盘,从3.2开始是50毫秒间隔。<br />
* 文档大小统计, totalSize = indexSize + dataSize, 而 storageSize 是实际占用大小,包括未使用的空间。<br />
<br />
== MMAPv1 引擎 ==<br />
* 使用 mmap() 将文件映射到内存。<br />
* 写数据是 60 秒,写日志是间隔 100 毫秒,通过 storage.syncPeriodSecs 和 storage.journal.commitIntervalMs 修改。<br />
* 文件比数据更大,因为:<br />
** 预分配文件,可以通过 storage.mmapv1.smallFiles 来减少初始文件大小,并限制最大 512m<br />
** oplog.rs 文件,也是预先创建的 capped collection,默认占用 5% 的磁盘空间(64位安装),通常不建议改这个。<br />
** 日志文件<br />
** 空记录,已删除的 collection 和文档都会保留在文件里。mongodb 会复用这些空间的,但是不会归还给 os。为了更有效的使用这些空间,你可以减少碎片,使用 compact 命令。compact 需要额外的 2g 磁盘空间来运行。compact 只是减少文件碎片,但是也不会归还空间给 os,想要归还可以用 repairDatabase 命令。repairDatabase 需要更大的磁盘空间来运行——等于你当前的数据集合大小,加上 2g。归还也可以通过重新同步来,特别是 secondary 成员。<br />
* page faults,发生在写入或者读取,当要写或者读的部分不在内存的时候发生,通常是因为内存不足引起的。分成 soft 和 hard, hard 读磁盘, soft 只是在 os 的文件缓存里移动。<br />
* 在更新的时候,如果文档增大, 文档可能需要在 disk 搬迁,为了减少这种影响, mongodb 使用 padding 对齐技术。对齐是自动的,也不建议手动。</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E7%AC%94%E8%AE%B0
Mongodb 存储引擎笔记
2017-05-23T03:24:34Z
<p>Dennis zhuang:/* MMAPv1 引擎 */</p>
<hr />
<div><br />
== 概述 ==<br />
<br />
* 同一个 replica set 可以使用不同的存储引擎。<br />
* 默认 wiredtiger 使用 snappy block compression;zlib 压缩算法,索引使用 prefix compression。<br />
* Starting in 3.4, the WiredTiger internal cache, by default, will use the larger of either: 50% of RAM minus 1 GB, or 256 MB.容器使用需要调整 storage.wiredTiger.engineConfig.cacheSizeGB<br />
* WT 引擎 60秒或者超过 2g 日志数据,写入一次 checkpoint<br />
* WT日志数据写入磁盘,从3.2开始是50毫秒间隔。<br />
<br />
== MMAPv1 引擎 ==<br />
* 使用 mmap() 将文件映射到内存。<br />
* 写数据是 60 秒,写日志是间隔 100 毫秒,通过 storage.syncPeriodSecs 和 storage.journal.commitIntervalMs 修改。<br />
* 文件比数据更大,因为:<br />
** 预分配文件,可以通过 storage.mmapv1.smallFiles 来减少初始文件大小,并限制最大 512m<br />
** oplog.rs 文件,也是预先创建的 capped collection,默认占用 5% 的磁盘空间(64位安装),通常不建议改这个。<br />
** 日志文件<br />
** 空记录,已删除的 collection 和文档都会保留在文件里。mongodb 会复用这些空间的,但是不会归还给 os。为了更有效的使用这些空间,你可以减少碎片,使用 compact 命令。compact 需要额外的 2g 磁盘空间来运行。compact 只是减少文件碎片,但是也不会归还空间给 os,想要归还可以用 repairDatabase 命令。repairDatabase 需要更大的磁盘空间来运行——等于你当前的数据集合大小,加上 2g。归还也可以通过重新同步来,特别是 secondary 成员。<br />
* page faults,发生在写入或者读取,当要写或者读的部分不在内存的时候发生,通常是因为内存不足引起的。分成 soft 和 hard, hard 读磁盘, soft 只是在 os 的文件缓存里移动。<br />
* 在更新的时候,如果文档增大, 文档可能需要在 disk 搬迁,为了减少这种影响, mongodb 使用 padding 对齐技术。对齐是自动的,也不建议手动。</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Mongodb_%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E7%AC%94%E8%AE%B0
Mongodb 存储引擎笔记
2017-05-23T03:11:40Z
<p>Dennis zhuang:以“ == 概述 == * 同一个 replica set 可以使用不同的存储引擎。 * 默认 wiredtiger 使用 snappy block compression;zlib 压缩算法,索引使用 pref...”为内容创建页面</p>
<hr />
<div><br />
== 概述 ==<br />
<br />
* 同一个 replica set 可以使用不同的存储引擎。<br />
* 默认 wiredtiger 使用 snappy block compression;zlib 压缩算法,索引使用 prefix compression。<br />
* Starting in 3.4, the WiredTiger internal cache, by default, will use the larger of either: 50% of RAM minus 1 GB, or 256 MB.容器使用需要调整 storage.wiredTiger.engineConfig.cacheSizeGB<br />
* WT 引擎 60秒或者超过 2g 日志数据,写入一次 checkpoint<br />
* WT日志数据写入磁盘,从3.2开始是50毫秒间隔。<br />
<br />
== MMAPv1 引擎 ==<br />
* 使用 mmap() 将文件映射到内存。<br />
* 写数据是 60 秒,写日志是间隔 100 毫秒,通过 storage.syncPeriodSecs 和 storage.journal.commitIntervalMs 修改。<br />
* 文件比数据更大,因为:<br />
** 预分配文件,可以通过 storage.mmapv1.smallFiles 来减少初始文件大小,并限制最大 512m<br />
** oplog.rs 文件,也是预先创建的 capped collection,默认占用 5% 的磁盘空间(64位安装),通常不建议改这个。<br />
** 日志文件<br />
** 空记录,已删除的 collection 和文档都会保留在文件里。mongodb 会复用这些空间的,但是不会归还给 os。为了更有效的使用这些空间,你可以减少碎片,使用 compact 命令。compact 需要额外的 2g 磁盘空间来运行。compact 只是减少文件碎片,但是也不会归还空间给 os,想要归还可以用 repairDatabase 命令。repairDatabase 需要更大的磁盘空间来运行——等于你当前的数据集合大小,加上 2g。归还也可以通过重新同步来,特别是 secondary 成员。</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=A_portable_compiler:_theory_and_practice
A portable compiler: theory and practice
2017-05-08T09:08:32Z
<p>Dennis zhuang:</p>
<hr />
<div><br />
== Overview of the amsterdam compiler kit ==<br />
<br />
<br />
The tool kit consists of eight components:<br />
• The preprocessor<br />
• The front ends<br />
• The peephole optimizer<br />
• The global optimizer<br />
• The back end<br />
• The target machine optimizer<br />
• The universal assembler/linker • The utility package<br />
<br />
[[文件:Amsterdam_compiler_kit.png]]<br />
<br />
== The preprocessor ==<br />
<br />
* extend all the program- ming languages by adding certain generally useful facilities to them in a uniform way. One of these is a simple macro system,<br />
* Another useful facility provided by the preprocessor is the ability to include compile-time libraries.<br />
* A third feature of the preprocessor is conditional compila- tion.<br />
<br />
== The front ends ==<br />
<br />
* All front ends, independent of the language being compiled, produce a common intermediate code called EM, which is the assembly language for a simple stack machine. The EM machine is based on a memory architecture containing a stack for local variables, a (static) data area for variables de- clared in the outermost block and global to the whole pro- gram, and a heap for dynamic data structures. <br />
*</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=A_portable_compiler:_theory_and_practice
A portable compiler: theory and practice
2017-05-08T08:18:15Z
<p>Dennis zhuang:</p>
<hr />
<div><br />
== Overview of the amsterdam compiler kit ==<br />
<br />
<br />
The tool kit consists of eight components:<br />
• The preprocessor<br />
• The front ends<br />
• The peephole optimizer<br />
• The global optimizer<br />
• The back end<br />
• The target machine optimizer<br />
• The universal assembler/linker • The utility package<br />
<br />
[[文件:Amsterdam_compiler_kit.png]]<br />
<br />
== The preprocessor ==<br />
<br />
* extend all the program- ming languages by adding certain generally useful facilities to them in a uniform way. One of these is a simple macro system,<br />
* Another useful facility provided by the preprocessor is the ability to include compile-time libraries.<br />
* A third feature of the preprocessor is conditional compila- tion.</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=A_portable_compiler:_theory_and_practice
A portable compiler: theory and practice
2017-05-08T08:07:22Z
<p>Dennis zhuang:</p>
<hr />
<div><br />
== Overview of the amsterdam compiler kit ==<br />
<br />
<br />
The tool kit consists of eight components:<br />
• The preprocessor<br />
• The front ends<br />
• The peephole optimizer<br />
• The global optimizer<br />
• The back end<br />
• The target machine optimizer<br />
• The universal assembler/linker • The utility package<br />
<br />
[[文件:Amsterdam_compiler_kit.png]]</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=%E6%96%87%E4%BB%B6:Amsterdam_compiler_kit.png
文件:Amsterdam compiler kit.png
2017-05-08T08:06:59Z
<p>Dennis zhuang:</p>
<hr />
<div></div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=A_portable_compiler:_theory_and_practice
A portable compiler: theory and practice
2017-05-08T08:05:58Z
<p>Dennis zhuang:</p>
<hr />
<div><br />
== Overview of the amsterdam compiler kit ==<br />
<br />
<br />
The tool kit consists of eight components:<br />
• The preprocessor<br />
• The front ends<br />
• The peephole optimizer<br />
• The global optimizer<br />
• The back end<br />
• The target machine optimizer<br />
• The universal assembler/linker • The utility package</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=A_portable_compiler:_theory_and_practice
A portable compiler: theory and practice
2017-05-08T08:05:45Z
<p>Dennis zhuang:</p>
<hr />
<div><br />
== Overview of the amsterdam compiler kit ==<br />
<br />
<br />
The tool kit consists of eight components:<br />
• The preprocessor<br />
• The front ends<br />
• The peephole optimizer<br />
• The global optimizer<br />
• The back end<br />
• The target machine optimizer<br />
• The universal assembler/linker • The utility package</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=A_portable_compiler:_theory_and_practice
A portable compiler: theory and practice
2017-05-08T08:05:37Z
<p>Dennis zhuang:以“ == Overview of the amsterdam compiler kit The tool kit consists of eight components: • The preprocessor • The front ends • The peephole optimizer • The gl...”为内容创建页面</p>
<hr />
<div><br />
== Overview of the amsterdam compiler kit<br />
<br />
<br />
The tool kit consists of eight components:<br />
• The preprocessor<br />
• The front ends<br />
• The peephole optimizer<br />
• The global optimizer<br />
• The back end<br />
• The target machine optimizer<br />
• The universal assembler/linker • The utility package</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-04-02T07:16:04Z
<p>Dennis zhuang:/* 元编程 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。参考 http://ju.outofmemory.cn/entry/135587<br />
<br />
* Ruby 的方法缓存包含两层:全局的方法缓存,用于缓存接收者和实现类之间的映射,因为方法查找是要遍历整个继承链的,缓存可以加速这个调用。其次是内联方法缓存,缓存 Ruby 已经执行的已编译的 YARV 指令信息,这样可以避免查找,加速的原理和 clojure 的 direct linking 技术是一样的。无论是定义新方法、include 模块或者其他类似的操作, Ruby 都会去清空这两个缓冲。<br />
* 多次include 不同模块,最近 include 的模块作为直接超类向上延伸。<br />
* 模块也可以 include 模块,规则与类 include 模块一致,也是副本插入作为超类,作为目标类和原始超类之间新的超类。<br />
* Module prepend 例子:<br />
<br />
<pre><br />
module Professor<br />
def name<br />
"Prof. #{super}"<br />
end<br />
end<br />
class Mathematician<br />
attr_accessor :name<br />
prepend Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.name = 'Johann Carl Friedrich Gauss'<br />
<br />
p p.name<br />
<br />
</pre><br />
<br />
* prepend 虽然仍然会将 Professor 设置为 Mathematician 的新超类,但是同时会拷贝一份 Mathematician 作为 Mathematician 原生类(Origin class),将这个原生类作为 Professor 的超类,这就可以解释为什么 Professor#name 的 super 能调用到 Mathematician 的 name 方法。参考 http://ju.outofmemory.cn/entry/135588<br />
* 修改已被 include 模块,比如增加方法,所有 include 该模块的类都将包含新方法,因为共享 m_tbl 方法表,Ruby 在 include 的时候拷贝的只是 RClass struct,不拷贝底层的方法表 ,看下面例子:<br />
<br />
<pre><br />
module Professor<br />
def letcures ; end<br />
end<br />
<br />
class Mathematician<br />
attr_accessor :first_name<br />
attr_accessor :last_name<br />
<br />
include Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.first_name = 'hello'<br />
p.last_name = 'world'<br />
<br />
p p.methods.sort<br />
<br />
#open Professor, adds new method<br />
module Professor<br />
def classroom; end<br />
end<br />
<br />
p p.methods.sort<br />
<br />
</pre><br />
<br />
* 但是修改已被 include 模块中 include 的其他模块,不会影响插入到 include 类的已经被拷贝的模块副本,也就不会增加或者删除方法。<br />
* 当创建一个 class或者模块的时候 ,其实是新建了一层词法作用域,Ruby 用两个指针来标示: nd_clss ,指向当前作用域对应的模块或者类;nd_next 指向父层或者上下文的词法作用域。形成一个作用域链条。<br />
* 常量的查找跟方法的查找类似,只是方法的查找是通过祖先连(super) 来查找,而常量是通过迭代词法作用域链(nd_next)来查找。<br />
* Ruby 优先通过词法作用域来查找常量:<br />
<br />
<pre><br />
class SuperClass<br />
FIND_ME = "Found in Superclass"<br />
end<br />
<br />
module ParentLexicalScope<br />
FIND_ME = "Found in ParentLexicalScope"<br />
<br />
module ChildLexicalScope<br />
<br />
class SubClass < SuperClass<br />
p FIND_ME<br />
end<br />
end<br />
end<br />
<br />
</pre><br />
<br />
输出 "Found in ParentLexicalScope"<br />
<br />
* 真实的 Ruby 常量查找还需要加入 autoload 关键字:<br />
<br />
『检索词法作用域链 -> 为了个作用域的类检查 autoload -> 检索超类链 -> 为每个超类检查 autoload -> 调用 const_missing。』。<br />
<br />
* 关于 autoload http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html<br />
<br />
== 散列表 ==<br />
<br />
* ruby 的散列表 hash 解决哈希冲突还是经典的链表法,密度阈值设定为 5,超过就做 rehash。也就是 java hash 的所谓负载因子。<br />
* rehash 扩容的大小不是翻倍之类的算法,而是基于素数,总是将容器数目设置为一个素数表里的素数大小。这个考虑也是基于对哈希函数不够分布的担忧。<br />
* 比较元素通过 eq? 方法。<br />
* 默认哈希算法采用 murmur hash,这跟 clojure 是一样的。自定义对象作为 key,同样也可以选择自定义实现 hash 函数。一般都推荐使用默认。<br />
* Ruby 2.0 做了个优化,跟 clojure 一样,小于等于 6 个元素的 hash 直接组织成一个数组, clojure 里是少于等于8个就是 PersistentArrayMap,节省内存和提升效率。<br />
* RHash 转移到 internal.h<br />
<br />
<pre><br />
struct RHash {<br />
struct RBasic basic;<br />
struct st_table *ntbl; /* possibly 0 */<br />
int iter_lev;<br />
const VALUE ifnone;<br />
};<br />
</pre><br />
<br />
* ruby 2.4 又有一个大的变化,使用开放地址法替换了链表法,参考 https://bugs.ruby-lang.org/issues/12142,这个讨论非常有价值。<br />
<br />
<pre><br />
/* The original package implemented classic bucket-based hash tables<br />
with entries doubly linked for an access by their insertion order.<br />
To decrease pointer chasing and as a consequence to improve a data<br />
locality the current implementation is based on storing entries in<br />
an array and using hash tables with open addressing. The current<br />
entries are more compact in comparison with the original ones and<br />
this also improves the data locality.<br />
<br />
The hash table has two arrays called *bins* and *entries*.<br />
<br />
bins:<br />
-------<br />
| | entries array:<br />
|-------| --------------------------------<br />
| index | | | entry: | | |<br />
|-------| | | | | |<br />
| ... | | ... | hash | ... | ... |<br />
|-------| | | key | | |<br />
| empty | | | record | | |<br />
|-------| --------------------------------<br />
| ... | ^ ^<br />
|-------| |_ entries start |_ entries bound<br />
|deleted|<br />
-------<br />
<br />
o The entry array contains table entries in the same order as they<br />
were inserted.<br />
<br />
When the first entry is deleted, a variable containing index of<br />
the current first entry (*entries start*) is changed. In all<br />
other cases of the deletion, we just mark the entry as deleted by<br />
using a reserved hash value.<br />
<br />
Such organization of the entry storage makes operations of the<br />
table shift and the entries traversal very fast.<br />
<br />
o The bins provide access to the entries by their keys. The<br />
key hash is mapped to a bin containing *index* of the<br />
corresponding entry in the entry array.<br />
<br />
The bin array size is always power of two, it makes mapping very<br />
fast by using the corresponding lower bits of the hash.<br />
Generally it is not a good idea to ignore some part of the hash.<br />
But alternative approach is worse. For example, we could use a<br />
modulo operation for mapping and a prime number for the size of<br />
the bin array. Unfortunately, the modulo operation for big<br />
64-bit numbers are extremely slow (it takes more than 100 cycles<br />
on modern Intel CPUs).<br />
<br />
Still other bits of the hash value are used when the mapping<br />
results in a collision. In this case we use a secondary hash<br />
value which is a result of a function of the collision bin<br />
index and the original hash value. The function choice<br />
guarantees that we can traverse all bins and finally find the<br />
corresponding bin as after several iterations the function<br />
becomes a full cycle linear congruential generator because it<br />
satisfies requirements of the Hull-Dobell theorem.<br />
<br />
When an entry is removed from the table besides marking the<br />
hash in the corresponding entry described above, we also mark<br />
the bin by a special value in order to find entries which had<br />
a collision with the removed entries.<br />
<br />
There are two reserved values for the bins. One denotes an<br />
empty bin, another one denotes a bin for a deleted entry.<br />
<br />
o The length of the bin array is at least two times more than the<br />
entry array length. This keeps the table load factor healthy.<br />
The trigger of rebuilding the table is always a case when we can<br />
not insert an entry anymore at the entries bound. We could<br />
change the entries bound too in case of deletion but than we need<br />
a special code to count bins with corresponding deleted entries<br />
and reset the bin values when there are too many bins<br />
corresponding deleted entries<br />
<br />
Table rebuilding is done by creation of a new entry array and<br />
bins of an appropriate size. We also try to reuse the arrays<br />
in some cases by compacting the array and removing deleted<br />
entries.<br />
<br />
o To save memory very small tables have no allocated arrays<br />
bins. We use a linear search for an access by a key.<br />
<br />
o To save more memory we use 8-, 16-, 32- and 64- bit indexes in<br />
bins depending on the current hash table size.<br />
<br />
This implementation speeds up the Ruby hash table benchmarks in<br />
average by more 40% on Intel Haswell CPU.<br />
<br />
*/<br />
<br />
</pre><br />
<br />
=== 2.4 st_table 阅读心得 ===<br />
* st_features 定义了 table 的属性:<br />
** entry_power entries数组的大小,2的幂指数。<br />
** bin_power bins数组大小,同样是2的幂指数<br />
** size_ind 根据 table 大小,选择 bins 对应的元素的大小,可能是 8-bit, 16-bit etc。<br />
** bins_words bins按照word计算的大小。<br />
** 根据 SIZEOF_ST_INDEX_T 枚举了一堆 table 属性。<br />
* 因为 bins 的大小都是 2 的次幂,因此计算哈希值对应的 bin 可以直接用位运算:<br />
<pre><br />
/* Return mask for a bin index in table TAB. */<br />
static inline st_index_t<br />
bins_mask(const st_table *tab)<br />
{<br />
return get_bins_num(tab) - 1;<br />
}<br />
<br />
/* Return the index of table TAB bin corresponding to<br />
HASH_VALUE. */<br />
static inline st_index_t<br />
hash_bin(st_hash_t hash_value, st_table *tab)<br />
{<br />
return hash_value & bins_mask(tab);<br />
}<br />
</pre><br />
* 最小的 table 大小是 4, 由 MINIMAL_POWER2 决定。最大的 table 大小是 2 的 30 次方(非 8 位平台),8 位平台上是 2 的 62 次方。<br />
* 对于小于等于 16 个元素的 table,不创建 bins 数组,直接存储在 entries 数组,线性探测,无需进行 hash 计算和查找。<br />
* rebuild_table 可能有两种: compact 现有的,或者创建新的。当 entries_bound 到达上限的时候,开始 rebuild。<br />
** 当已有 entries 数组的大小在现有元素大小的 2 倍到 4 倍(REBUILD_THRESHOLD)之间,或者元素数量小于 16 个,进入压缩流程,直接使用原来 table 作为新 new_tab<br />
** 否则,进入新建 table 作为 new_tab<br />
** 小技巧 , prefetch 指令,预加载下个元素,在遍历 entries 的时候用到。<br />
<pre><br />
#define PREFETCH(addr, write_p) __builtin_prefetch(addr, write_p)<br />
PREFETCH(entries + i + 1, 0);<br />
</pre><br />
** rehash 其实很简单了,遍历已有的 entries 数组,跳过已经删除的,设置到新的 table 里,同时设置 bins:<br />
<pre><br />
bins = new_tab->bins;<br />
size_ind = get_size_ind(new_tab);<br />
for (i = tab->entries_start; i < bound; i++) {<br />
curr_entry_ptr = &entries[i];<br />
PREFETCH(entries + i + 1, 0);<br />
if (EXPECT(DELETED_ENTRY_P(curr_entry_ptr), 0))<br />
continue;<br />
if (&new_entries[ni] != curr_entry_ptr)<br />
new_entries[ni] = *curr_entry_ptr;<br />
if (EXPECT(bins != NULL, 1)) {<br />
bin_ind = find_table_bin_ind_direct(new_tab, curr_entry_ptr->hash,<br />
curr_entry_ptr->key);<br />
st_assert(bin_ind != UNDEFINED_BIN_IND<br />
&& (tab == new_tab || new_tab->rebuilds_num == 0)<br />
&& IND_EMPTY_BIN_P(new_tab, bin_ind));<br />
set_bin(bins, size_ind, bin_ind, ni + ENTRY_BASE);<br />
}<br />
new_tab->num_entries++;<br />
ni++;<br />
}<br />
<br />
</pre><br />
<br />
* 开放地址法,遇到哈希冲突,采用二次哈希,次级哈希函数如下:<br />
<pre><br />
/* Return the next secondary hash index for table TAB using previous<br />
index IND and PERTERB. Finally modulo of the function becomes a<br />
full *cycle linear congruential generator*, in other words it<br />
guarantees traversing all table bins in extreme case.<br />
<br />
According the Hull-Dobell theorem a generator<br />
"Xnext = (a*Xprev + c) mod m" is a full cycle generator iff<br />
o m and c are relatively prime<br />
o a-1 is divisible by all prime factors of m<br />
o a-1 is divisible by 4 if m is divisible by 4.<br />
<br />
For our case a is 5, c is 1, and m is a power of two. */<br />
static inline st_index_t<br />
secondary_hash(st_index_t ind, st_table *tab, st_index_t *perterb)<br />
{<br />
*perterb >>= 11;<br />
ind = (ind << 2) + ind + *perterb + 1;<br />
return hash_bin(ind, tab);<br />
}<br />
<br />
</pre><br />
<br />
* find_entry 线性探测,find_table_entry_ind 是二次哈希查找,但是可以选择是否启用二次探测:<br />
<pre><br />
/* Use the quadratic probing. The method has a better data locality<br />
but more collisions than the current approach. In average it<br />
results in a bit slower search. */<br />
/*#define QUADRATIC_PROBE*/<br />
<br />
ind = hash_bin(hash_value, tab);<br />
#ifdef QUADRATIC_PROBE<br />
d = 1;<br />
#else<br />
peterb = hash_value;<br />
#endif<br />
FOUND_BIN;<br />
for (;;) {<br />
bin = get_bin(tab->bins, get_size_ind(tab), ind);<br />
//找到相等的。<br />
if (! EMPTY_OR_DELETED_BIN_P(bin)<br />
&& PTR_EQUAL(tab, &entries[bin - ENTRY_BASE], hash_value, key))<br />
break;<br />
//或者找到空的。<br />
else if (EMPTY_BIN_P(bin))<br />
return UNDEFINED_ENTRY_IND;<br />
#ifdef QUADRATIC_PROBE<br />
//启用了二次探测,计算下一个探测位置。<br />
ind = hash_bin(ind + d, tab);<br />
d++;<br />
#else<br />
//或者二次哈希<br />
ind = secondary_hash(ind, tab, &peterb);<br />
#endif<br />
COLLISION;<br />
}<br />
return bin;<br />
</pre><br />
<br />
默认采用二次哈希。<br />
<br />
* 查找过程简单明了:<br />
<br />
<pre><br />
/* Find an entry with KEY in table TAB. Return non-zero if we found<br />
it. Set up *RECORD to the found entry record. */<br />
int<br />
st_lookup(st_table *tab, st_data_t key, st_data_t *value)<br />
{<br />
st_index_t bin;<br />
//计算哈希<br />
st_hash_t hash = do_hash(key, tab);<br />
<br />
if (tab->bins == NULL) {<br />
//线性查找,对于少于等于 16 个元素的table<br />
bin = find_entry(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
} else {<br />
//进入哈希查找。<br />
bin = find_table_entry_ind(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
bin -= ENTRY_BASE;<br />
}<br />
//赋值,返回<br />
if (value != 0)<br />
*value = tab->entries[bin].record;<br />
return 1;<br />
}<br />
<br />
</pre><br />
<br />
* 删除是标记删除,开放地址法必须这么做,否则冲突的时候,前面的删除,就找不到后面的元素了:<br />
<pre><br />
bin = get_bin(tab->bins, get_size_ind(tab), bin_ind) - ENTRY_BASE;<br />
MARK_BIN_DELETED(tab, bin_ind);<br />
}<br />
entry = &tab->entries[bin];<br />
*key = entry->key;<br />
if (value != 0) *value = entry->record;<br />
MARK_ENTRY_DELETED(entry);<br />
</pre><br />
<br />
== 闭包 ==<br />
* rb_block_struct 和 rb_control_frame_struct 两个结构,闭包本质是代码块和它的上下文环境(EP指针)。rb_block_t 结构体是 rb_control_frame_t 的一部分,避免分配。但是在最新代码里已经修改了, rb_struct 变成一个 union,其中 rb_captured_block 是 rb_control_frame_struct 一部分 :<br />
<br />
<pre><br />
typedef struct rb_control_frame_struct {<br />
const VALUE *pc; /* cfp[0] */<br />
VALUE *sp; /* cfp[1] */<br />
const rb_iseq_t *iseq; /* cfp[2] */<br />
VALUE self; /* cfp[3] / block[0] */<br />
const VALUE *ep; /* cfp[4] / block[1] */<br />
const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */<br />
<br />
#if VM_DEBUG_BP_CHECK<br />
VALUE *bp_check; /* cfp[6] */<br />
#endif<br />
} rb_control_frame_t;<br />
<br />
<br />
enum rb_block_type {<br />
block_type_iseq,<br />
block_type_ifunc,<br />
block_type_symbol,<br />
block_type_proc<br />
};<br />
<br />
struct rb_block {<br />
union {<br />
struct rb_captured_block captured;<br />
VALUE symbol;<br />
VALUE proc;<br />
} as;<br />
enum rb_block_type type;<br />
};<br />
<br />
typedef struct rb_control_frame_struct {<br />
const VALUE *pc; /* cfp[0] */<br />
VALUE *sp; /* cfp[1] */<br />
const rb_iseq_t *iseq; /* cfp[2] */<br />
VALUE self; /* cfp[3] / block[0] */<br />
const VALUE *ep; /* cfp[4] / block[1] */<br />
const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */<br />
<br />
#if VM_DEBUG_BP_CHECK<br />
VALUE *bp_check; /* cfp[6] */<br />
#endif<br />
} rb_control_frame_t;<br />
<br />
</pre><br />
<br />
* 上述代码中比较有意思的是 rb_block_type 分成四种。<br />
* 从测试来看(2.4), each block 确实会比 while 循环慢(在我的机器上是 60%)。因为block 需要做更多的工作:创建栈帧、拷贝 EP 指针等。<br />
* 创建 proc 的时候,会复制一份当前的栈帧副本 rb_env_t,proc 和 lambda 本质上是一样的,通过一个布尔值区分 is_lambda:<br />
<br />
<pre><br />
typedef struct {<br />
const struct rb_block block;<br />
int8_t safe_level; /* 0..1 */<br />
int8_t is_from_method; /* bool */<br />
int8_t is_lambda; /* bool */<br />
} rb_proc_t;<br />
<br />
typedef struct {<br />
VALUE flags; /* imemo header */<br />
const rb_iseq_t *iseq;<br />
const VALUE *ep;<br />
const VALUE *env;<br />
unsigned int env_size;<br />
} rb_env_t;<br />
<br />
</pre><br />
<br />
* proc 的创建是通过 RTypedData 和 rb_proc_t 结合来创建的:<br />
<br />
<pre><br />
struct RTypedData {<br />
struct RBasic basic;<br />
const rb_data_type_t *type;<br />
VALUE typed_flag; /* 1 or not */<br />
void *data;<br />
};<br />
<br />
/* Proc */<br />
<br />
VALUE<br />
rb_proc_create_from_captured(VALUE klass,<br />
const struct rb_captured_block *captured,<br />
enum rb_block_type block_type,<br />
int8_t safe_level, int8_t is_from_method, int8_t is_lambda)<br />
{<br />
VALUE procval = rb_proc_alloc(klass);<br />
rb_proc_t *proc = RTYPEDDATA_DATA(procval);<br />
<br />
VM_ASSERT(VM_EP_IN_HEAP_P(GET_THREAD(), captured->ep));<br />
<br />
/* copy block */<br />
RB_OBJ_WRITE(procval, &proc->block.as.captured.self, captured->self);<br />
RB_OBJ_WRITE(procval, &proc->block.as.captured.code.val, captured->code.val);<br />
*((const VALUE **)&proc->block.as.captured.ep) = captured->ep;<br />
RB_OBJ_WRITTEN(procval, Qundef, VM_ENV_ENVVAL(captured->ep));<br />
<br />
vm_block_type_set(&proc->block, block_type);<br />
proc->safe_level = safe_level;<br />
proc->is_from_method = is_from_method;<br />
proc->is_lambda = is_lambda;<br />
<br />
return procval;<br />
}<br />
</pre><br />
<br />
tagged struct 也是常见的 c 语言技巧。<br />
<br />
== 元编程 ==<br />
* class 关键字,开启一个新的词法作用域。<br />
* class 里 def 定义方法,三个步骤:编译代码指令、使用当前的词法作用域获取类或者模块的指针、在该类的方法表中保存新的方法。<br />
* 类方法 def self.xxx 是定义在类的元类里,也就是 Quote.singleton_class。<br />
* class << 类, 也是在元类定义方法,比之 self.xxx 主要是方便,<< 开启了一个新的词法作用域。<br />
* def 对象.xxx,会为对象创建新的单类(singleton class),并为它指派新的方法。<br />
* class << 对象与 def 对象.xxx 类似。<br />
* Ruby 2.0 引入 refine ,可以在某个模块中不修改原始类的情况下重新定义它的方法:<br />
<br />
<pre><br />
class Quote<br />
def display<br />
puts "The quick brown fox jumped over the lazy dog."<br />
end<br />
end<br />
<br />
module AllCaps<br />
refine Quote do<br />
def display<br />
puts "THE QUICK BROWN FOX JUMPED OVER THE LAZY DOG."<br />
end<br />
end<br />
end<br />
def test<br />
Quote.new.display<br />
using AllCaps<br />
Quote.new.display<br />
end<br />
<br />
Quote.new.display<br />
using AllCaps<br />
Quote.new.display<br />
<br />
test<br />
<br />
</pre><br />
<br />
using 暂时不允许使用在顶级作用域。using 相当于在当前作用域激活了 refinement 模块,方法查找将从当前激活的 refinement 模块开始,没有找到才去调用原始方法。<br />
<br />
* 词法作用域本质是是两个指针:nd_class 当前 class, nd_next 上一层作用域,可以通过 Module.nesting 来查看嵌套作用域。<br />
* def 定义的方法都是当前的词法作用域的类上,也就是 self,顶层作用域定义的方法其实都是在 Object 类里。<br />
* self 的规则: 在类或者模块的作用域内部,self 总是被设置为类或者模块。在方法内部(包括类方法),self 被设置为方法调用的接收者 receiver。<br />
* eval 会创建闭包:编译代码和当前环境。<br />
* self 是闭包的一部分, 对象的 instance_eval 体现了这一点。<br />
* 对象 instance_eval 会创建新的词法作用域,并且设置 self 为对象的单类。<br />
* define_method 和 def 的区别在于 define_method 会创建闭包,因此可以访问外层的环境,而 def 不行,所以用 define_method 来打破界限。<br />
<br />
== 垃圾回收 ==<br />
<br />
* MRI 使用的标记清除算法,在标记和清除的时候会暂停程序,在 1.9.3 开始引入了延迟清除(lazy sweep)的优化,降低垃圾回收每次带来的暂停时间,但是并不会减少整个垃圾回收的工作量。<br />
* 可以通过 GC.start 来强制发起 full gc<br />
* GC::Profiler.enable 和 GC::Profiler.report 提供了 GC 报告,总体上来说 gc 消耗的时间跟堆的大小成线性关系。<br />
* JRuby 的 gc 就是 JVM 的 gc,比如复制收集、分代收集、并发收集等等,不再重复。</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-04-02T07:05:00Z
<p>Dennis zhuang:/* 元编程 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。参考 http://ju.outofmemory.cn/entry/135587<br />
<br />
* Ruby 的方法缓存包含两层:全局的方法缓存,用于缓存接收者和实现类之间的映射,因为方法查找是要遍历整个继承链的,缓存可以加速这个调用。其次是内联方法缓存,缓存 Ruby 已经执行的已编译的 YARV 指令信息,这样可以避免查找,加速的原理和 clojure 的 direct linking 技术是一样的。无论是定义新方法、include 模块或者其他类似的操作, Ruby 都会去清空这两个缓冲。<br />
* 多次include 不同模块,最近 include 的模块作为直接超类向上延伸。<br />
* 模块也可以 include 模块,规则与类 include 模块一致,也是副本插入作为超类,作为目标类和原始超类之间新的超类。<br />
* Module prepend 例子:<br />
<br />
<pre><br />
module Professor<br />
def name<br />
"Prof. #{super}"<br />
end<br />
end<br />
class Mathematician<br />
attr_accessor :name<br />
prepend Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.name = 'Johann Carl Friedrich Gauss'<br />
<br />
p p.name<br />
<br />
</pre><br />
<br />
* prepend 虽然仍然会将 Professor 设置为 Mathematician 的新超类,但是同时会拷贝一份 Mathematician 作为 Mathematician 原生类(Origin class),将这个原生类作为 Professor 的超类,这就可以解释为什么 Professor#name 的 super 能调用到 Mathematician 的 name 方法。参考 http://ju.outofmemory.cn/entry/135588<br />
* 修改已被 include 模块,比如增加方法,所有 include 该模块的类都将包含新方法,因为共享 m_tbl 方法表,Ruby 在 include 的时候拷贝的只是 RClass struct,不拷贝底层的方法表 ,看下面例子:<br />
<br />
<pre><br />
module Professor<br />
def letcures ; end<br />
end<br />
<br />
class Mathematician<br />
attr_accessor :first_name<br />
attr_accessor :last_name<br />
<br />
include Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.first_name = 'hello'<br />
p.last_name = 'world'<br />
<br />
p p.methods.sort<br />
<br />
#open Professor, adds new method<br />
module Professor<br />
def classroom; end<br />
end<br />
<br />
p p.methods.sort<br />
<br />
</pre><br />
<br />
* 但是修改已被 include 模块中 include 的其他模块,不会影响插入到 include 类的已经被拷贝的模块副本,也就不会增加或者删除方法。<br />
* 当创建一个 class或者模块的时候 ,其实是新建了一层词法作用域,Ruby 用两个指针来标示: nd_clss ,指向当前作用域对应的模块或者类;nd_next 指向父层或者上下文的词法作用域。形成一个作用域链条。<br />
* 常量的查找跟方法的查找类似,只是方法的查找是通过祖先连(super) 来查找,而常量是通过迭代词法作用域链(nd_next)来查找。<br />
* Ruby 优先通过词法作用域来查找常量:<br />
<br />
<pre><br />
class SuperClass<br />
FIND_ME = "Found in Superclass"<br />
end<br />
<br />
module ParentLexicalScope<br />
FIND_ME = "Found in ParentLexicalScope"<br />
<br />
module ChildLexicalScope<br />
<br />
class SubClass < SuperClass<br />
p FIND_ME<br />
end<br />
end<br />
end<br />
<br />
</pre><br />
<br />
输出 "Found in ParentLexicalScope"<br />
<br />
* 真实的 Ruby 常量查找还需要加入 autoload 关键字:<br />
<br />
『检索词法作用域链 -> 为了个作用域的类检查 autoload -> 检索超类链 -> 为每个超类检查 autoload -> 调用 const_missing。』。<br />
<br />
* 关于 autoload http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html<br />
<br />
== 散列表 ==<br />
<br />
* ruby 的散列表 hash 解决哈希冲突还是经典的链表法,密度阈值设定为 5,超过就做 rehash。也就是 java hash 的所谓负载因子。<br />
* rehash 扩容的大小不是翻倍之类的算法,而是基于素数,总是将容器数目设置为一个素数表里的素数大小。这个考虑也是基于对哈希函数不够分布的担忧。<br />
* 比较元素通过 eq? 方法。<br />
* 默认哈希算法采用 murmur hash,这跟 clojure 是一样的。自定义对象作为 key,同样也可以选择自定义实现 hash 函数。一般都推荐使用默认。<br />
* Ruby 2.0 做了个优化,跟 clojure 一样,小于等于 6 个元素的 hash 直接组织成一个数组, clojure 里是少于等于8个就是 PersistentArrayMap,节省内存和提升效率。<br />
* RHash 转移到 internal.h<br />
<br />
<pre><br />
struct RHash {<br />
struct RBasic basic;<br />
struct st_table *ntbl; /* possibly 0 */<br />
int iter_lev;<br />
const VALUE ifnone;<br />
};<br />
</pre><br />
<br />
* ruby 2.4 又有一个大的变化,使用开放地址法替换了链表法,参考 https://bugs.ruby-lang.org/issues/12142,这个讨论非常有价值。<br />
<br />
<pre><br />
/* The original package implemented classic bucket-based hash tables<br />
with entries doubly linked for an access by their insertion order.<br />
To decrease pointer chasing and as a consequence to improve a data<br />
locality the current implementation is based on storing entries in<br />
an array and using hash tables with open addressing. The current<br />
entries are more compact in comparison with the original ones and<br />
this also improves the data locality.<br />
<br />
The hash table has two arrays called *bins* and *entries*.<br />
<br />
bins:<br />
-------<br />
| | entries array:<br />
|-------| --------------------------------<br />
| index | | | entry: | | |<br />
|-------| | | | | |<br />
| ... | | ... | hash | ... | ... |<br />
|-------| | | key | | |<br />
| empty | | | record | | |<br />
|-------| --------------------------------<br />
| ... | ^ ^<br />
|-------| |_ entries start |_ entries bound<br />
|deleted|<br />
-------<br />
<br />
o The entry array contains table entries in the same order as they<br />
were inserted.<br />
<br />
When the first entry is deleted, a variable containing index of<br />
the current first entry (*entries start*) is changed. In all<br />
other cases of the deletion, we just mark the entry as deleted by<br />
using a reserved hash value.<br />
<br />
Such organization of the entry storage makes operations of the<br />
table shift and the entries traversal very fast.<br />
<br />
o The bins provide access to the entries by their keys. The<br />
key hash is mapped to a bin containing *index* of the<br />
corresponding entry in the entry array.<br />
<br />
The bin array size is always power of two, it makes mapping very<br />
fast by using the corresponding lower bits of the hash.<br />
Generally it is not a good idea to ignore some part of the hash.<br />
But alternative approach is worse. For example, we could use a<br />
modulo operation for mapping and a prime number for the size of<br />
the bin array. Unfortunately, the modulo operation for big<br />
64-bit numbers are extremely slow (it takes more than 100 cycles<br />
on modern Intel CPUs).<br />
<br />
Still other bits of the hash value are used when the mapping<br />
results in a collision. In this case we use a secondary hash<br />
value which is a result of a function of the collision bin<br />
index and the original hash value. The function choice<br />
guarantees that we can traverse all bins and finally find the<br />
corresponding bin as after several iterations the function<br />
becomes a full cycle linear congruential generator because it<br />
satisfies requirements of the Hull-Dobell theorem.<br />
<br />
When an entry is removed from the table besides marking the<br />
hash in the corresponding entry described above, we also mark<br />
the bin by a special value in order to find entries which had<br />
a collision with the removed entries.<br />
<br />
There are two reserved values for the bins. One denotes an<br />
empty bin, another one denotes a bin for a deleted entry.<br />
<br />
o The length of the bin array is at least two times more than the<br />
entry array length. This keeps the table load factor healthy.<br />
The trigger of rebuilding the table is always a case when we can<br />
not insert an entry anymore at the entries bound. We could<br />
change the entries bound too in case of deletion but than we need<br />
a special code to count bins with corresponding deleted entries<br />
and reset the bin values when there are too many bins<br />
corresponding deleted entries<br />
<br />
Table rebuilding is done by creation of a new entry array and<br />
bins of an appropriate size. We also try to reuse the arrays<br />
in some cases by compacting the array and removing deleted<br />
entries.<br />
<br />
o To save memory very small tables have no allocated arrays<br />
bins. We use a linear search for an access by a key.<br />
<br />
o To save more memory we use 8-, 16-, 32- and 64- bit indexes in<br />
bins depending on the current hash table size.<br />
<br />
This implementation speeds up the Ruby hash table benchmarks in<br />
average by more 40% on Intel Haswell CPU.<br />
<br />
*/<br />
<br />
</pre><br />
<br />
=== 2.4 st_table 阅读心得 ===<br />
* st_features 定义了 table 的属性:<br />
** entry_power entries数组的大小,2的幂指数。<br />
** bin_power bins数组大小,同样是2的幂指数<br />
** size_ind 根据 table 大小,选择 bins 对应的元素的大小,可能是 8-bit, 16-bit etc。<br />
** bins_words bins按照word计算的大小。<br />
** 根据 SIZEOF_ST_INDEX_T 枚举了一堆 table 属性。<br />
* 因为 bins 的大小都是 2 的次幂,因此计算哈希值对应的 bin 可以直接用位运算:<br />
<pre><br />
/* Return mask for a bin index in table TAB. */<br />
static inline st_index_t<br />
bins_mask(const st_table *tab)<br />
{<br />
return get_bins_num(tab) - 1;<br />
}<br />
<br />
/* Return the index of table TAB bin corresponding to<br />
HASH_VALUE. */<br />
static inline st_index_t<br />
hash_bin(st_hash_t hash_value, st_table *tab)<br />
{<br />
return hash_value & bins_mask(tab);<br />
}<br />
</pre><br />
* 最小的 table 大小是 4, 由 MINIMAL_POWER2 决定。最大的 table 大小是 2 的 30 次方(非 8 位平台),8 位平台上是 2 的 62 次方。<br />
* 对于小于等于 16 个元素的 table,不创建 bins 数组,直接存储在 entries 数组,线性探测,无需进行 hash 计算和查找。<br />
* rebuild_table 可能有两种: compact 现有的,或者创建新的。当 entries_bound 到达上限的时候,开始 rebuild。<br />
** 当已有 entries 数组的大小在现有元素大小的 2 倍到 4 倍(REBUILD_THRESHOLD)之间,或者元素数量小于 16 个,进入压缩流程,直接使用原来 table 作为新 new_tab<br />
** 否则,进入新建 table 作为 new_tab<br />
** 小技巧 , prefetch 指令,预加载下个元素,在遍历 entries 的时候用到。<br />
<pre><br />
#define PREFETCH(addr, write_p) __builtin_prefetch(addr, write_p)<br />
PREFETCH(entries + i + 1, 0);<br />
</pre><br />
** rehash 其实很简单了,遍历已有的 entries 数组,跳过已经删除的,设置到新的 table 里,同时设置 bins:<br />
<pre><br />
bins = new_tab->bins;<br />
size_ind = get_size_ind(new_tab);<br />
for (i = tab->entries_start; i < bound; i++) {<br />
curr_entry_ptr = &entries[i];<br />
PREFETCH(entries + i + 1, 0);<br />
if (EXPECT(DELETED_ENTRY_P(curr_entry_ptr), 0))<br />
continue;<br />
if (&new_entries[ni] != curr_entry_ptr)<br />
new_entries[ni] = *curr_entry_ptr;<br />
if (EXPECT(bins != NULL, 1)) {<br />
bin_ind = find_table_bin_ind_direct(new_tab, curr_entry_ptr->hash,<br />
curr_entry_ptr->key);<br />
st_assert(bin_ind != UNDEFINED_BIN_IND<br />
&& (tab == new_tab || new_tab->rebuilds_num == 0)<br />
&& IND_EMPTY_BIN_P(new_tab, bin_ind));<br />
set_bin(bins, size_ind, bin_ind, ni + ENTRY_BASE);<br />
}<br />
new_tab->num_entries++;<br />
ni++;<br />
}<br />
<br />
</pre><br />
<br />
* 开放地址法,遇到哈希冲突,采用二次哈希,次级哈希函数如下:<br />
<pre><br />
/* Return the next secondary hash index for table TAB using previous<br />
index IND and PERTERB. Finally modulo of the function becomes a<br />
full *cycle linear congruential generator*, in other words it<br />
guarantees traversing all table bins in extreme case.<br />
<br />
According the Hull-Dobell theorem a generator<br />
"Xnext = (a*Xprev + c) mod m" is a full cycle generator iff<br />
o m and c are relatively prime<br />
o a-1 is divisible by all prime factors of m<br />
o a-1 is divisible by 4 if m is divisible by 4.<br />
<br />
For our case a is 5, c is 1, and m is a power of two. */<br />
static inline st_index_t<br />
secondary_hash(st_index_t ind, st_table *tab, st_index_t *perterb)<br />
{<br />
*perterb >>= 11;<br />
ind = (ind << 2) + ind + *perterb + 1;<br />
return hash_bin(ind, tab);<br />
}<br />
<br />
</pre><br />
<br />
* find_entry 线性探测,find_table_entry_ind 是二次哈希查找,但是可以选择是否启用二次探测:<br />
<pre><br />
/* Use the quadratic probing. The method has a better data locality<br />
but more collisions than the current approach. In average it<br />
results in a bit slower search. */<br />
/*#define QUADRATIC_PROBE*/<br />
<br />
ind = hash_bin(hash_value, tab);<br />
#ifdef QUADRATIC_PROBE<br />
d = 1;<br />
#else<br />
peterb = hash_value;<br />
#endif<br />
FOUND_BIN;<br />
for (;;) {<br />
bin = get_bin(tab->bins, get_size_ind(tab), ind);<br />
//找到相等的。<br />
if (! EMPTY_OR_DELETED_BIN_P(bin)<br />
&& PTR_EQUAL(tab, &entries[bin - ENTRY_BASE], hash_value, key))<br />
break;<br />
//或者找到空的。<br />
else if (EMPTY_BIN_P(bin))<br />
return UNDEFINED_ENTRY_IND;<br />
#ifdef QUADRATIC_PROBE<br />
//启用了二次探测,计算下一个探测位置。<br />
ind = hash_bin(ind + d, tab);<br />
d++;<br />
#else<br />
//或者二次哈希<br />
ind = secondary_hash(ind, tab, &peterb);<br />
#endif<br />
COLLISION;<br />
}<br />
return bin;<br />
</pre><br />
<br />
默认采用二次哈希。<br />
<br />
* 查找过程简单明了:<br />
<br />
<pre><br />
/* Find an entry with KEY in table TAB. Return non-zero if we found<br />
it. Set up *RECORD to the found entry record. */<br />
int<br />
st_lookup(st_table *tab, st_data_t key, st_data_t *value)<br />
{<br />
st_index_t bin;<br />
//计算哈希<br />
st_hash_t hash = do_hash(key, tab);<br />
<br />
if (tab->bins == NULL) {<br />
//线性查找,对于少于等于 16 个元素的table<br />
bin = find_entry(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
} else {<br />
//进入哈希查找。<br />
bin = find_table_entry_ind(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
bin -= ENTRY_BASE;<br />
}<br />
//赋值,返回<br />
if (value != 0)<br />
*value = tab->entries[bin].record;<br />
return 1;<br />
}<br />
<br />
</pre><br />
<br />
* 删除是标记删除,开放地址法必须这么做,否则冲突的时候,前面的删除,就找不到后面的元素了:<br />
<pre><br />
bin = get_bin(tab->bins, get_size_ind(tab), bin_ind) - ENTRY_BASE;<br />
MARK_BIN_DELETED(tab, bin_ind);<br />
}<br />
entry = &tab->entries[bin];<br />
*key = entry->key;<br />
if (value != 0) *value = entry->record;<br />
MARK_ENTRY_DELETED(entry);<br />
</pre><br />
<br />
== 闭包 ==<br />
* rb_block_struct 和 rb_control_frame_struct 两个结构,闭包本质是代码块和它的上下文环境(EP指针)。rb_block_t 结构体是 rb_control_frame_t 的一部分,避免分配。但是在最新代码里已经修改了, rb_struct 变成一个 union,其中 rb_captured_block 是 rb_control_frame_struct 一部分 :<br />
<br />
<pre><br />
typedef struct rb_control_frame_struct {<br />
const VALUE *pc; /* cfp[0] */<br />
VALUE *sp; /* cfp[1] */<br />
const rb_iseq_t *iseq; /* cfp[2] */<br />
VALUE self; /* cfp[3] / block[0] */<br />
const VALUE *ep; /* cfp[4] / block[1] */<br />
const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */<br />
<br />
#if VM_DEBUG_BP_CHECK<br />
VALUE *bp_check; /* cfp[6] */<br />
#endif<br />
} rb_control_frame_t;<br />
<br />
<br />
enum rb_block_type {<br />
block_type_iseq,<br />
block_type_ifunc,<br />
block_type_symbol,<br />
block_type_proc<br />
};<br />
<br />
struct rb_block {<br />
union {<br />
struct rb_captured_block captured;<br />
VALUE symbol;<br />
VALUE proc;<br />
} as;<br />
enum rb_block_type type;<br />
};<br />
<br />
typedef struct rb_control_frame_struct {<br />
const VALUE *pc; /* cfp[0] */<br />
VALUE *sp; /* cfp[1] */<br />
const rb_iseq_t *iseq; /* cfp[2] */<br />
VALUE self; /* cfp[3] / block[0] */<br />
const VALUE *ep; /* cfp[4] / block[1] */<br />
const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */<br />
<br />
#if VM_DEBUG_BP_CHECK<br />
VALUE *bp_check; /* cfp[6] */<br />
#endif<br />
} rb_control_frame_t;<br />
<br />
</pre><br />
<br />
* 上述代码中比较有意思的是 rb_block_type 分成四种。<br />
* 从测试来看(2.4), each block 确实会比 while 循环慢(在我的机器上是 60%)。因为block 需要做更多的工作:创建栈帧、拷贝 EP 指针等。<br />
* 创建 proc 的时候,会复制一份当前的栈帧副本 rb_env_t,proc 和 lambda 本质上是一样的,通过一个布尔值区分 is_lambda:<br />
<br />
<pre><br />
typedef struct {<br />
const struct rb_block block;<br />
int8_t safe_level; /* 0..1 */<br />
int8_t is_from_method; /* bool */<br />
int8_t is_lambda; /* bool */<br />
} rb_proc_t;<br />
<br />
typedef struct {<br />
VALUE flags; /* imemo header */<br />
const rb_iseq_t *iseq;<br />
const VALUE *ep;<br />
const VALUE *env;<br />
unsigned int env_size;<br />
} rb_env_t;<br />
<br />
</pre><br />
<br />
* proc 的创建是通过 RTypedData 和 rb_proc_t 结合来创建的:<br />
<br />
<pre><br />
struct RTypedData {<br />
struct RBasic basic;<br />
const rb_data_type_t *type;<br />
VALUE typed_flag; /* 1 or not */<br />
void *data;<br />
};<br />
<br />
/* Proc */<br />
<br />
VALUE<br />
rb_proc_create_from_captured(VALUE klass,<br />
const struct rb_captured_block *captured,<br />
enum rb_block_type block_type,<br />
int8_t safe_level, int8_t is_from_method, int8_t is_lambda)<br />
{<br />
VALUE procval = rb_proc_alloc(klass);<br />
rb_proc_t *proc = RTYPEDDATA_DATA(procval);<br />
<br />
VM_ASSERT(VM_EP_IN_HEAP_P(GET_THREAD(), captured->ep));<br />
<br />
/* copy block */<br />
RB_OBJ_WRITE(procval, &proc->block.as.captured.self, captured->self);<br />
RB_OBJ_WRITE(procval, &proc->block.as.captured.code.val, captured->code.val);<br />
*((const VALUE **)&proc->block.as.captured.ep) = captured->ep;<br />
RB_OBJ_WRITTEN(procval, Qundef, VM_ENV_ENVVAL(captured->ep));<br />
<br />
vm_block_type_set(&proc->block, block_type);<br />
proc->safe_level = safe_level;<br />
proc->is_from_method = is_from_method;<br />
proc->is_lambda = is_lambda;<br />
<br />
return procval;<br />
}<br />
</pre><br />
<br />
tagged struct 也是常见的 c 语言技巧。<br />
<br />
== 元编程 ==<br />
* class 关键字,开启一个新的词法作用域。<br />
* class 里 def 定义方法,三个步骤:编译代码指令、使用当前的词法作用域获取类或者模块的指针、在该类的方法表中保存新的方法。<br />
* 类方法 def self.xxx 是定义在类的元类里,也就是 Quote.singleton_class。<br />
* class << 类, 也是在元类定义方法,比之 self.xxx 主要是方便,<< 开启了一个新的词法作用域。<br />
* def 对象.xxx,会为对象创建新的单类(singleton class),并为它指派新的方法。<br />
* class << 对象与 def 对象.xxx 类似。<br />
* Ruby 2.0 引入 refine ,可以在某个模块中不修改原始类的情况下重新定义它的方法:<br />
<br />
<pre><br />
class Quote<br />
def display<br />
puts "The quick brown fox jumped over the lazy dog."<br />
end<br />
end<br />
<br />
module AllCaps<br />
refine Quote do<br />
def display<br />
puts "THE QUICK BROWN FOX JUMPED OVER THE LAZY DOG."<br />
end<br />
end<br />
end<br />
def test<br />
Quote.new.display<br />
using AllCaps<br />
Quote.new.display<br />
end<br />
<br />
Quote.new.display<br />
using AllCaps<br />
Quote.new.display<br />
<br />
test<br />
<br />
</pre><br />
<br />
using 暂时不允许使用在顶级作用域。using 相当于在当前作用域激活了 refinement 模块,方法查找将从当前激活的 refinement 模块开始,没有找到才去调用原始方法。<br />
<br />
* 词法作用域本质是是两个指针:nd_class 当前 class, nd_next 上一层作用域,可以通过 Module.nesting 来查看嵌套作用域。<br />
* def 定义的方法都是当前的词法作用域的类上,也就是 self,顶层作用域定义的方法其实都是在 Object 类里。<br />
* self 的规则: 在类或者模块的作用域内部,self 总是被设置为类或者模块。在方法内部(包括类方法),self 被设置为方法调用的接收者 receiver。<br />
* eval 会创建闭包:编译代码和当前环境。<br />
* self 是闭包的一部分, 对象的 instance_eval 体现了这一点。<br />
<br />
== 垃圾回收 ==<br />
<br />
* MRI 使用的标记清除算法,在标记和清除的时候会暂停程序,在 1.9.3 开始引入了延迟清除(lazy sweep)的优化,降低垃圾回收每次带来的暂停时间,但是并不会减少整个垃圾回收的工作量。<br />
* 可以通过 GC.start 来强制发起 full gc<br />
* GC::Profiler.enable 和 GC::Profiler.report 提供了 GC 报告,总体上来说 gc 消耗的时间跟堆的大小成线性关系。<br />
* JRuby 的 gc 就是 JVM 的 gc,比如复制收集、分代收集、并发收集等等,不再重复。</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-04-02T06:56:44Z
<p>Dennis zhuang:/* 元编程 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。参考 http://ju.outofmemory.cn/entry/135587<br />
<br />
* Ruby 的方法缓存包含两层:全局的方法缓存,用于缓存接收者和实现类之间的映射,因为方法查找是要遍历整个继承链的,缓存可以加速这个调用。其次是内联方法缓存,缓存 Ruby 已经执行的已编译的 YARV 指令信息,这样可以避免查找,加速的原理和 clojure 的 direct linking 技术是一样的。无论是定义新方法、include 模块或者其他类似的操作, Ruby 都会去清空这两个缓冲。<br />
* 多次include 不同模块,最近 include 的模块作为直接超类向上延伸。<br />
* 模块也可以 include 模块,规则与类 include 模块一致,也是副本插入作为超类,作为目标类和原始超类之间新的超类。<br />
* Module prepend 例子:<br />
<br />
<pre><br />
module Professor<br />
def name<br />
"Prof. #{super}"<br />
end<br />
end<br />
class Mathematician<br />
attr_accessor :name<br />
prepend Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.name = 'Johann Carl Friedrich Gauss'<br />
<br />
p p.name<br />
<br />
</pre><br />
<br />
* prepend 虽然仍然会将 Professor 设置为 Mathematician 的新超类,但是同时会拷贝一份 Mathematician 作为 Mathematician 原生类(Origin class),将这个原生类作为 Professor 的超类,这就可以解释为什么 Professor#name 的 super 能调用到 Mathematician 的 name 方法。参考 http://ju.outofmemory.cn/entry/135588<br />
* 修改已被 include 模块,比如增加方法,所有 include 该模块的类都将包含新方法,因为共享 m_tbl 方法表,Ruby 在 include 的时候拷贝的只是 RClass struct,不拷贝底层的方法表 ,看下面例子:<br />
<br />
<pre><br />
module Professor<br />
def letcures ; end<br />
end<br />
<br />
class Mathematician<br />
attr_accessor :first_name<br />
attr_accessor :last_name<br />
<br />
include Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.first_name = 'hello'<br />
p.last_name = 'world'<br />
<br />
p p.methods.sort<br />
<br />
#open Professor, adds new method<br />
module Professor<br />
def classroom; end<br />
end<br />
<br />
p p.methods.sort<br />
<br />
</pre><br />
<br />
* 但是修改已被 include 模块中 include 的其他模块,不会影响插入到 include 类的已经被拷贝的模块副本,也就不会增加或者删除方法。<br />
* 当创建一个 class或者模块的时候 ,其实是新建了一层词法作用域,Ruby 用两个指针来标示: nd_clss ,指向当前作用域对应的模块或者类;nd_next 指向父层或者上下文的词法作用域。形成一个作用域链条。<br />
* 常量的查找跟方法的查找类似,只是方法的查找是通过祖先连(super) 来查找,而常量是通过迭代词法作用域链(nd_next)来查找。<br />
* Ruby 优先通过词法作用域来查找常量:<br />
<br />
<pre><br />
class SuperClass<br />
FIND_ME = "Found in Superclass"<br />
end<br />
<br />
module ParentLexicalScope<br />
FIND_ME = "Found in ParentLexicalScope"<br />
<br />
module ChildLexicalScope<br />
<br />
class SubClass < SuperClass<br />
p FIND_ME<br />
end<br />
end<br />
end<br />
<br />
</pre><br />
<br />
输出 "Found in ParentLexicalScope"<br />
<br />
* 真实的 Ruby 常量查找还需要加入 autoload 关键字:<br />
<br />
『检索词法作用域链 -> 为了个作用域的类检查 autoload -> 检索超类链 -> 为每个超类检查 autoload -> 调用 const_missing。』。<br />
<br />
* 关于 autoload http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html<br />
<br />
== 散列表 ==<br />
<br />
* ruby 的散列表 hash 解决哈希冲突还是经典的链表法,密度阈值设定为 5,超过就做 rehash。也就是 java hash 的所谓负载因子。<br />
* rehash 扩容的大小不是翻倍之类的算法,而是基于素数,总是将容器数目设置为一个素数表里的素数大小。这个考虑也是基于对哈希函数不够分布的担忧。<br />
* 比较元素通过 eq? 方法。<br />
* 默认哈希算法采用 murmur hash,这跟 clojure 是一样的。自定义对象作为 key,同样也可以选择自定义实现 hash 函数。一般都推荐使用默认。<br />
* Ruby 2.0 做了个优化,跟 clojure 一样,小于等于 6 个元素的 hash 直接组织成一个数组, clojure 里是少于等于8个就是 PersistentArrayMap,节省内存和提升效率。<br />
* RHash 转移到 internal.h<br />
<br />
<pre><br />
struct RHash {<br />
struct RBasic basic;<br />
struct st_table *ntbl; /* possibly 0 */<br />
int iter_lev;<br />
const VALUE ifnone;<br />
};<br />
</pre><br />
<br />
* ruby 2.4 又有一个大的变化,使用开放地址法替换了链表法,参考 https://bugs.ruby-lang.org/issues/12142,这个讨论非常有价值。<br />
<br />
<pre><br />
/* The original package implemented classic bucket-based hash tables<br />
with entries doubly linked for an access by their insertion order.<br />
To decrease pointer chasing and as a consequence to improve a data<br />
locality the current implementation is based on storing entries in<br />
an array and using hash tables with open addressing. The current<br />
entries are more compact in comparison with the original ones and<br />
this also improves the data locality.<br />
<br />
The hash table has two arrays called *bins* and *entries*.<br />
<br />
bins:<br />
-------<br />
| | entries array:<br />
|-------| --------------------------------<br />
| index | | | entry: | | |<br />
|-------| | | | | |<br />
| ... | | ... | hash | ... | ... |<br />
|-------| | | key | | |<br />
| empty | | | record | | |<br />
|-------| --------------------------------<br />
| ... | ^ ^<br />
|-------| |_ entries start |_ entries bound<br />
|deleted|<br />
-------<br />
<br />
o The entry array contains table entries in the same order as they<br />
were inserted.<br />
<br />
When the first entry is deleted, a variable containing index of<br />
the current first entry (*entries start*) is changed. In all<br />
other cases of the deletion, we just mark the entry as deleted by<br />
using a reserved hash value.<br />
<br />
Such organization of the entry storage makes operations of the<br />
table shift and the entries traversal very fast.<br />
<br />
o The bins provide access to the entries by their keys. The<br />
key hash is mapped to a bin containing *index* of the<br />
corresponding entry in the entry array.<br />
<br />
The bin array size is always power of two, it makes mapping very<br />
fast by using the corresponding lower bits of the hash.<br />
Generally it is not a good idea to ignore some part of the hash.<br />
But alternative approach is worse. For example, we could use a<br />
modulo operation for mapping and a prime number for the size of<br />
the bin array. Unfortunately, the modulo operation for big<br />
64-bit numbers are extremely slow (it takes more than 100 cycles<br />
on modern Intel CPUs).<br />
<br />
Still other bits of the hash value are used when the mapping<br />
results in a collision. In this case we use a secondary hash<br />
value which is a result of a function of the collision bin<br />
index and the original hash value. The function choice<br />
guarantees that we can traverse all bins and finally find the<br />
corresponding bin as after several iterations the function<br />
becomes a full cycle linear congruential generator because it<br />
satisfies requirements of the Hull-Dobell theorem.<br />
<br />
When an entry is removed from the table besides marking the<br />
hash in the corresponding entry described above, we also mark<br />
the bin by a special value in order to find entries which had<br />
a collision with the removed entries.<br />
<br />
There are two reserved values for the bins. One denotes an<br />
empty bin, another one denotes a bin for a deleted entry.<br />
<br />
o The length of the bin array is at least two times more than the<br />
entry array length. This keeps the table load factor healthy.<br />
The trigger of rebuilding the table is always a case when we can<br />
not insert an entry anymore at the entries bound. We could<br />
change the entries bound too in case of deletion but than we need<br />
a special code to count bins with corresponding deleted entries<br />
and reset the bin values when there are too many bins<br />
corresponding deleted entries<br />
<br />
Table rebuilding is done by creation of a new entry array and<br />
bins of an appropriate size. We also try to reuse the arrays<br />
in some cases by compacting the array and removing deleted<br />
entries.<br />
<br />
o To save memory very small tables have no allocated arrays<br />
bins. We use a linear search for an access by a key.<br />
<br />
o To save more memory we use 8-, 16-, 32- and 64- bit indexes in<br />
bins depending on the current hash table size.<br />
<br />
This implementation speeds up the Ruby hash table benchmarks in<br />
average by more 40% on Intel Haswell CPU.<br />
<br />
*/<br />
<br />
</pre><br />
<br />
=== 2.4 st_table 阅读心得 ===<br />
* st_features 定义了 table 的属性:<br />
** entry_power entries数组的大小,2的幂指数。<br />
** bin_power bins数组大小,同样是2的幂指数<br />
** size_ind 根据 table 大小,选择 bins 对应的元素的大小,可能是 8-bit, 16-bit etc。<br />
** bins_words bins按照word计算的大小。<br />
** 根据 SIZEOF_ST_INDEX_T 枚举了一堆 table 属性。<br />
* 因为 bins 的大小都是 2 的次幂,因此计算哈希值对应的 bin 可以直接用位运算:<br />
<pre><br />
/* Return mask for a bin index in table TAB. */<br />
static inline st_index_t<br />
bins_mask(const st_table *tab)<br />
{<br />
return get_bins_num(tab) - 1;<br />
}<br />
<br />
/* Return the index of table TAB bin corresponding to<br />
HASH_VALUE. */<br />
static inline st_index_t<br />
hash_bin(st_hash_t hash_value, st_table *tab)<br />
{<br />
return hash_value & bins_mask(tab);<br />
}<br />
</pre><br />
* 最小的 table 大小是 4, 由 MINIMAL_POWER2 决定。最大的 table 大小是 2 的 30 次方(非 8 位平台),8 位平台上是 2 的 62 次方。<br />
* 对于小于等于 16 个元素的 table,不创建 bins 数组,直接存储在 entries 数组,线性探测,无需进行 hash 计算和查找。<br />
* rebuild_table 可能有两种: compact 现有的,或者创建新的。当 entries_bound 到达上限的时候,开始 rebuild。<br />
** 当已有 entries 数组的大小在现有元素大小的 2 倍到 4 倍(REBUILD_THRESHOLD)之间,或者元素数量小于 16 个,进入压缩流程,直接使用原来 table 作为新 new_tab<br />
** 否则,进入新建 table 作为 new_tab<br />
** 小技巧 , prefetch 指令,预加载下个元素,在遍历 entries 的时候用到。<br />
<pre><br />
#define PREFETCH(addr, write_p) __builtin_prefetch(addr, write_p)<br />
PREFETCH(entries + i + 1, 0);<br />
</pre><br />
** rehash 其实很简单了,遍历已有的 entries 数组,跳过已经删除的,设置到新的 table 里,同时设置 bins:<br />
<pre><br />
bins = new_tab->bins;<br />
size_ind = get_size_ind(new_tab);<br />
for (i = tab->entries_start; i < bound; i++) {<br />
curr_entry_ptr = &entries[i];<br />
PREFETCH(entries + i + 1, 0);<br />
if (EXPECT(DELETED_ENTRY_P(curr_entry_ptr), 0))<br />
continue;<br />
if (&new_entries[ni] != curr_entry_ptr)<br />
new_entries[ni] = *curr_entry_ptr;<br />
if (EXPECT(bins != NULL, 1)) {<br />
bin_ind = find_table_bin_ind_direct(new_tab, curr_entry_ptr->hash,<br />
curr_entry_ptr->key);<br />
st_assert(bin_ind != UNDEFINED_BIN_IND<br />
&& (tab == new_tab || new_tab->rebuilds_num == 0)<br />
&& IND_EMPTY_BIN_P(new_tab, bin_ind));<br />
set_bin(bins, size_ind, bin_ind, ni + ENTRY_BASE);<br />
}<br />
new_tab->num_entries++;<br />
ni++;<br />
}<br />
<br />
</pre><br />
<br />
* 开放地址法,遇到哈希冲突,采用二次哈希,次级哈希函数如下:<br />
<pre><br />
/* Return the next secondary hash index for table TAB using previous<br />
index IND and PERTERB. Finally modulo of the function becomes a<br />
full *cycle linear congruential generator*, in other words it<br />
guarantees traversing all table bins in extreme case.<br />
<br />
According the Hull-Dobell theorem a generator<br />
"Xnext = (a*Xprev + c) mod m" is a full cycle generator iff<br />
o m and c are relatively prime<br />
o a-1 is divisible by all prime factors of m<br />
o a-1 is divisible by 4 if m is divisible by 4.<br />
<br />
For our case a is 5, c is 1, and m is a power of two. */<br />
static inline st_index_t<br />
secondary_hash(st_index_t ind, st_table *tab, st_index_t *perterb)<br />
{<br />
*perterb >>= 11;<br />
ind = (ind << 2) + ind + *perterb + 1;<br />
return hash_bin(ind, tab);<br />
}<br />
<br />
</pre><br />
<br />
* find_entry 线性探测,find_table_entry_ind 是二次哈希查找,但是可以选择是否启用二次探测:<br />
<pre><br />
/* Use the quadratic probing. The method has a better data locality<br />
but more collisions than the current approach. In average it<br />
results in a bit slower search. */<br />
/*#define QUADRATIC_PROBE*/<br />
<br />
ind = hash_bin(hash_value, tab);<br />
#ifdef QUADRATIC_PROBE<br />
d = 1;<br />
#else<br />
peterb = hash_value;<br />
#endif<br />
FOUND_BIN;<br />
for (;;) {<br />
bin = get_bin(tab->bins, get_size_ind(tab), ind);<br />
//找到相等的。<br />
if (! EMPTY_OR_DELETED_BIN_P(bin)<br />
&& PTR_EQUAL(tab, &entries[bin - ENTRY_BASE], hash_value, key))<br />
break;<br />
//或者找到空的。<br />
else if (EMPTY_BIN_P(bin))<br />
return UNDEFINED_ENTRY_IND;<br />
#ifdef QUADRATIC_PROBE<br />
//启用了二次探测,计算下一个探测位置。<br />
ind = hash_bin(ind + d, tab);<br />
d++;<br />
#else<br />
//或者二次哈希<br />
ind = secondary_hash(ind, tab, &peterb);<br />
#endif<br />
COLLISION;<br />
}<br />
return bin;<br />
</pre><br />
<br />
默认采用二次哈希。<br />
<br />
* 查找过程简单明了:<br />
<br />
<pre><br />
/* Find an entry with KEY in table TAB. Return non-zero if we found<br />
it. Set up *RECORD to the found entry record. */<br />
int<br />
st_lookup(st_table *tab, st_data_t key, st_data_t *value)<br />
{<br />
st_index_t bin;<br />
//计算哈希<br />
st_hash_t hash = do_hash(key, tab);<br />
<br />
if (tab->bins == NULL) {<br />
//线性查找,对于少于等于 16 个元素的table<br />
bin = find_entry(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
} else {<br />
//进入哈希查找。<br />
bin = find_table_entry_ind(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
bin -= ENTRY_BASE;<br />
}<br />
//赋值,返回<br />
if (value != 0)<br />
*value = tab->entries[bin].record;<br />
return 1;<br />
}<br />
<br />
</pre><br />
<br />
* 删除是标记删除,开放地址法必须这么做,否则冲突的时候,前面的删除,就找不到后面的元素了:<br />
<pre><br />
bin = get_bin(tab->bins, get_size_ind(tab), bin_ind) - ENTRY_BASE;<br />
MARK_BIN_DELETED(tab, bin_ind);<br />
}<br />
entry = &tab->entries[bin];<br />
*key = entry->key;<br />
if (value != 0) *value = entry->record;<br />
MARK_ENTRY_DELETED(entry);<br />
</pre><br />
<br />
== 闭包 ==<br />
* rb_block_struct 和 rb_control_frame_struct 两个结构,闭包本质是代码块和它的上下文环境(EP指针)。rb_block_t 结构体是 rb_control_frame_t 的一部分,避免分配。但是在最新代码里已经修改了, rb_struct 变成一个 union,其中 rb_captured_block 是 rb_control_frame_struct 一部分 :<br />
<br />
<pre><br />
typedef struct rb_control_frame_struct {<br />
const VALUE *pc; /* cfp[0] */<br />
VALUE *sp; /* cfp[1] */<br />
const rb_iseq_t *iseq; /* cfp[2] */<br />
VALUE self; /* cfp[3] / block[0] */<br />
const VALUE *ep; /* cfp[4] / block[1] */<br />
const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */<br />
<br />
#if VM_DEBUG_BP_CHECK<br />
VALUE *bp_check; /* cfp[6] */<br />
#endif<br />
} rb_control_frame_t;<br />
<br />
<br />
enum rb_block_type {<br />
block_type_iseq,<br />
block_type_ifunc,<br />
block_type_symbol,<br />
block_type_proc<br />
};<br />
<br />
struct rb_block {<br />
union {<br />
struct rb_captured_block captured;<br />
VALUE symbol;<br />
VALUE proc;<br />
} as;<br />
enum rb_block_type type;<br />
};<br />
<br />
typedef struct rb_control_frame_struct {<br />
const VALUE *pc; /* cfp[0] */<br />
VALUE *sp; /* cfp[1] */<br />
const rb_iseq_t *iseq; /* cfp[2] */<br />
VALUE self; /* cfp[3] / block[0] */<br />
const VALUE *ep; /* cfp[4] / block[1] */<br />
const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */<br />
<br />
#if VM_DEBUG_BP_CHECK<br />
VALUE *bp_check; /* cfp[6] */<br />
#endif<br />
} rb_control_frame_t;<br />
<br />
</pre><br />
<br />
* 上述代码中比较有意思的是 rb_block_type 分成四种。<br />
* 从测试来看(2.4), each block 确实会比 while 循环慢(在我的机器上是 60%)。因为block 需要做更多的工作:创建栈帧、拷贝 EP 指针等。<br />
* 创建 proc 的时候,会复制一份当前的栈帧副本 rb_env_t,proc 和 lambda 本质上是一样的,通过一个布尔值区分 is_lambda:<br />
<br />
<pre><br />
typedef struct {<br />
const struct rb_block block;<br />
int8_t safe_level; /* 0..1 */<br />
int8_t is_from_method; /* bool */<br />
int8_t is_lambda; /* bool */<br />
} rb_proc_t;<br />
<br />
typedef struct {<br />
VALUE flags; /* imemo header */<br />
const rb_iseq_t *iseq;<br />
const VALUE *ep;<br />
const VALUE *env;<br />
unsigned int env_size;<br />
} rb_env_t;<br />
<br />
</pre><br />
<br />
* proc 的创建是通过 RTypedData 和 rb_proc_t 结合来创建的:<br />
<br />
<pre><br />
struct RTypedData {<br />
struct RBasic basic;<br />
const rb_data_type_t *type;<br />
VALUE typed_flag; /* 1 or not */<br />
void *data;<br />
};<br />
<br />
/* Proc */<br />
<br />
VALUE<br />
rb_proc_create_from_captured(VALUE klass,<br />
const struct rb_captured_block *captured,<br />
enum rb_block_type block_type,<br />
int8_t safe_level, int8_t is_from_method, int8_t is_lambda)<br />
{<br />
VALUE procval = rb_proc_alloc(klass);<br />
rb_proc_t *proc = RTYPEDDATA_DATA(procval);<br />
<br />
VM_ASSERT(VM_EP_IN_HEAP_P(GET_THREAD(), captured->ep));<br />
<br />
/* copy block */<br />
RB_OBJ_WRITE(procval, &proc->block.as.captured.self, captured->self);<br />
RB_OBJ_WRITE(procval, &proc->block.as.captured.code.val, captured->code.val);<br />
*((const VALUE **)&proc->block.as.captured.ep) = captured->ep;<br />
RB_OBJ_WRITTEN(procval, Qundef, VM_ENV_ENVVAL(captured->ep));<br />
<br />
vm_block_type_set(&proc->block, block_type);<br />
proc->safe_level = safe_level;<br />
proc->is_from_method = is_from_method;<br />
proc->is_lambda = is_lambda;<br />
<br />
return procval;<br />
}<br />
</pre><br />
<br />
tagged struct 也是常见的 c 语言技巧。<br />
<br />
== 元编程 ==<br />
* class 关键字,开启一个新的词法作用域。<br />
* class 里 def 定义方法,三个步骤:编译代码指令、使用当前的词法作用域获取类或者模块的指针、在该类的方法表中保存新的方法。<br />
* 类方法 def self.xxx 是定义在类的元类里,也就是 Quote.singleton_class。<br />
* class << 类, 也是在元类定义方法,比之 self.xxx 主要是方便,<< 开启了一个新的词法作用域。<br />
* def 对象.xxx,会为对象创建新的单类(singleton class),并为它指派新的方法。<br />
* class << 对象与 def 对象.xxx 类似。<br />
* Ruby 2.0 引入 refine ,可以在某个模块中不修改原始类的情况下重新定义它的方法:<br />
<br />
<pre><br />
class Quote<br />
def display<br />
puts "The quick brown fox jumped over the lazy dog."<br />
end<br />
end<br />
<br />
module AllCaps<br />
refine Quote do<br />
def display<br />
puts "THE QUICK BROWN FOX JUMPED OVER THE LAZY DOG."<br />
end<br />
end<br />
end<br />
def test<br />
Quote.new.display<br />
using AllCaps<br />
Quote.new.display<br />
end<br />
<br />
Quote.new.display<br />
using AllCaps<br />
Quote.new.display<br />
<br />
test<br />
<br />
</pre><br />
<br />
using 暂时不允许使用在顶级作用域。using 相当于在当前作用域激活了 refinement 模块,方法查找将从当前激活的 refinement 模块开始,没有找到才去调用原始方法。<br />
<br />
* 词法作用域本质是是两个指针:nd_class 当前 class, nd_next 上一层作用域,可以通过 Module.nesting 来查看嵌套作用域。<br />
* def 定义的方法都是当前的词法作用域的类上,也就是 self,顶层作用域定义的方法其实都是在 Object 类里。<br />
<br />
== 垃圾回收 ==<br />
<br />
* MRI 使用的标记清除算法,在标记和清除的时候会暂停程序,在 1.9.3 开始引入了延迟清除(lazy sweep)的优化,降低垃圾回收每次带来的暂停时间,但是并不会减少整个垃圾回收的工作量。<br />
* 可以通过 GC.start 来强制发起 full gc<br />
* GC::Profiler.enable 和 GC::Profiler.report 提供了 GC 报告,总体上来说 gc 消耗的时间跟堆的大小成线性关系。<br />
* JRuby 的 gc 就是 JVM 的 gc,比如复制收集、分代收集、并发收集等等,不再重复。</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-04-02T06:53:29Z
<p>Dennis zhuang:/* 元编程 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。参考 http://ju.outofmemory.cn/entry/135587<br />
<br />
* Ruby 的方法缓存包含两层:全局的方法缓存,用于缓存接收者和实现类之间的映射,因为方法查找是要遍历整个继承链的,缓存可以加速这个调用。其次是内联方法缓存,缓存 Ruby 已经执行的已编译的 YARV 指令信息,这样可以避免查找,加速的原理和 clojure 的 direct linking 技术是一样的。无论是定义新方法、include 模块或者其他类似的操作, Ruby 都会去清空这两个缓冲。<br />
* 多次include 不同模块,最近 include 的模块作为直接超类向上延伸。<br />
* 模块也可以 include 模块,规则与类 include 模块一致,也是副本插入作为超类,作为目标类和原始超类之间新的超类。<br />
* Module prepend 例子:<br />
<br />
<pre><br />
module Professor<br />
def name<br />
"Prof. #{super}"<br />
end<br />
end<br />
class Mathematician<br />
attr_accessor :name<br />
prepend Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.name = 'Johann Carl Friedrich Gauss'<br />
<br />
p p.name<br />
<br />
</pre><br />
<br />
* prepend 虽然仍然会将 Professor 设置为 Mathematician 的新超类,但是同时会拷贝一份 Mathematician 作为 Mathematician 原生类(Origin class),将这个原生类作为 Professor 的超类,这就可以解释为什么 Professor#name 的 super 能调用到 Mathematician 的 name 方法。参考 http://ju.outofmemory.cn/entry/135588<br />
* 修改已被 include 模块,比如增加方法,所有 include 该模块的类都将包含新方法,因为共享 m_tbl 方法表,Ruby 在 include 的时候拷贝的只是 RClass struct,不拷贝底层的方法表 ,看下面例子:<br />
<br />
<pre><br />
module Professor<br />
def letcures ; end<br />
end<br />
<br />
class Mathematician<br />
attr_accessor :first_name<br />
attr_accessor :last_name<br />
<br />
include Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.first_name = 'hello'<br />
p.last_name = 'world'<br />
<br />
p p.methods.sort<br />
<br />
#open Professor, adds new method<br />
module Professor<br />
def classroom; end<br />
end<br />
<br />
p p.methods.sort<br />
<br />
</pre><br />
<br />
* 但是修改已被 include 模块中 include 的其他模块,不会影响插入到 include 类的已经被拷贝的模块副本,也就不会增加或者删除方法。<br />
* 当创建一个 class或者模块的时候 ,其实是新建了一层词法作用域,Ruby 用两个指针来标示: nd_clss ,指向当前作用域对应的模块或者类;nd_next 指向父层或者上下文的词法作用域。形成一个作用域链条。<br />
* 常量的查找跟方法的查找类似,只是方法的查找是通过祖先连(super) 来查找,而常量是通过迭代词法作用域链(nd_next)来查找。<br />
* Ruby 优先通过词法作用域来查找常量:<br />
<br />
<pre><br />
class SuperClass<br />
FIND_ME = "Found in Superclass"<br />
end<br />
<br />
module ParentLexicalScope<br />
FIND_ME = "Found in ParentLexicalScope"<br />
<br />
module ChildLexicalScope<br />
<br />
class SubClass < SuperClass<br />
p FIND_ME<br />
end<br />
end<br />
end<br />
<br />
</pre><br />
<br />
输出 "Found in ParentLexicalScope"<br />
<br />
* 真实的 Ruby 常量查找还需要加入 autoload 关键字:<br />
<br />
『检索词法作用域链 -> 为了个作用域的类检查 autoload -> 检索超类链 -> 为每个超类检查 autoload -> 调用 const_missing。』。<br />
<br />
* 关于 autoload http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html<br />
<br />
== 散列表 ==<br />
<br />
* ruby 的散列表 hash 解决哈希冲突还是经典的链表法,密度阈值设定为 5,超过就做 rehash。也就是 java hash 的所谓负载因子。<br />
* rehash 扩容的大小不是翻倍之类的算法,而是基于素数,总是将容器数目设置为一个素数表里的素数大小。这个考虑也是基于对哈希函数不够分布的担忧。<br />
* 比较元素通过 eq? 方法。<br />
* 默认哈希算法采用 murmur hash,这跟 clojure 是一样的。自定义对象作为 key,同样也可以选择自定义实现 hash 函数。一般都推荐使用默认。<br />
* Ruby 2.0 做了个优化,跟 clojure 一样,小于等于 6 个元素的 hash 直接组织成一个数组, clojure 里是少于等于8个就是 PersistentArrayMap,节省内存和提升效率。<br />
* RHash 转移到 internal.h<br />
<br />
<pre><br />
struct RHash {<br />
struct RBasic basic;<br />
struct st_table *ntbl; /* possibly 0 */<br />
int iter_lev;<br />
const VALUE ifnone;<br />
};<br />
</pre><br />
<br />
* ruby 2.4 又有一个大的变化,使用开放地址法替换了链表法,参考 https://bugs.ruby-lang.org/issues/12142,这个讨论非常有价值。<br />
<br />
<pre><br />
/* The original package implemented classic bucket-based hash tables<br />
with entries doubly linked for an access by their insertion order.<br />
To decrease pointer chasing and as a consequence to improve a data<br />
locality the current implementation is based on storing entries in<br />
an array and using hash tables with open addressing. The current<br />
entries are more compact in comparison with the original ones and<br />
this also improves the data locality.<br />
<br />
The hash table has two arrays called *bins* and *entries*.<br />
<br />
bins:<br />
-------<br />
| | entries array:<br />
|-------| --------------------------------<br />
| index | | | entry: | | |<br />
|-------| | | | | |<br />
| ... | | ... | hash | ... | ... |<br />
|-------| | | key | | |<br />
| empty | | | record | | |<br />
|-------| --------------------------------<br />
| ... | ^ ^<br />
|-------| |_ entries start |_ entries bound<br />
|deleted|<br />
-------<br />
<br />
o The entry array contains table entries in the same order as they<br />
were inserted.<br />
<br />
When the first entry is deleted, a variable containing index of<br />
the current first entry (*entries start*) is changed. In all<br />
other cases of the deletion, we just mark the entry as deleted by<br />
using a reserved hash value.<br />
<br />
Such organization of the entry storage makes operations of the<br />
table shift and the entries traversal very fast.<br />
<br />
o The bins provide access to the entries by their keys. The<br />
key hash is mapped to a bin containing *index* of the<br />
corresponding entry in the entry array.<br />
<br />
The bin array size is always power of two, it makes mapping very<br />
fast by using the corresponding lower bits of the hash.<br />
Generally it is not a good idea to ignore some part of the hash.<br />
But alternative approach is worse. For example, we could use a<br />
modulo operation for mapping and a prime number for the size of<br />
the bin array. Unfortunately, the modulo operation for big<br />
64-bit numbers are extremely slow (it takes more than 100 cycles<br />
on modern Intel CPUs).<br />
<br />
Still other bits of the hash value are used when the mapping<br />
results in a collision. In this case we use a secondary hash<br />
value which is a result of a function of the collision bin<br />
index and the original hash value. The function choice<br />
guarantees that we can traverse all bins and finally find the<br />
corresponding bin as after several iterations the function<br />
becomes a full cycle linear congruential generator because it<br />
satisfies requirements of the Hull-Dobell theorem.<br />
<br />
When an entry is removed from the table besides marking the<br />
hash in the corresponding entry described above, we also mark<br />
the bin by a special value in order to find entries which had<br />
a collision with the removed entries.<br />
<br />
There are two reserved values for the bins. One denotes an<br />
empty bin, another one denotes a bin for a deleted entry.<br />
<br />
o The length of the bin array is at least two times more than the<br />
entry array length. This keeps the table load factor healthy.<br />
The trigger of rebuilding the table is always a case when we can<br />
not insert an entry anymore at the entries bound. We could<br />
change the entries bound too in case of deletion but than we need<br />
a special code to count bins with corresponding deleted entries<br />
and reset the bin values when there are too many bins<br />
corresponding deleted entries<br />
<br />
Table rebuilding is done by creation of a new entry array and<br />
bins of an appropriate size. We also try to reuse the arrays<br />
in some cases by compacting the array and removing deleted<br />
entries.<br />
<br />
o To save memory very small tables have no allocated arrays<br />
bins. We use a linear search for an access by a key.<br />
<br />
o To save more memory we use 8-, 16-, 32- and 64- bit indexes in<br />
bins depending on the current hash table size.<br />
<br />
This implementation speeds up the Ruby hash table benchmarks in<br />
average by more 40% on Intel Haswell CPU.<br />
<br />
*/<br />
<br />
</pre><br />
<br />
=== 2.4 st_table 阅读心得 ===<br />
* st_features 定义了 table 的属性:<br />
** entry_power entries数组的大小,2的幂指数。<br />
** bin_power bins数组大小,同样是2的幂指数<br />
** size_ind 根据 table 大小,选择 bins 对应的元素的大小,可能是 8-bit, 16-bit etc。<br />
** bins_words bins按照word计算的大小。<br />
** 根据 SIZEOF_ST_INDEX_T 枚举了一堆 table 属性。<br />
* 因为 bins 的大小都是 2 的次幂,因此计算哈希值对应的 bin 可以直接用位运算:<br />
<pre><br />
/* Return mask for a bin index in table TAB. */<br />
static inline st_index_t<br />
bins_mask(const st_table *tab)<br />
{<br />
return get_bins_num(tab) - 1;<br />
}<br />
<br />
/* Return the index of table TAB bin corresponding to<br />
HASH_VALUE. */<br />
static inline st_index_t<br />
hash_bin(st_hash_t hash_value, st_table *tab)<br />
{<br />
return hash_value & bins_mask(tab);<br />
}<br />
</pre><br />
* 最小的 table 大小是 4, 由 MINIMAL_POWER2 决定。最大的 table 大小是 2 的 30 次方(非 8 位平台),8 位平台上是 2 的 62 次方。<br />
* 对于小于等于 16 个元素的 table,不创建 bins 数组,直接存储在 entries 数组,线性探测,无需进行 hash 计算和查找。<br />
* rebuild_table 可能有两种: compact 现有的,或者创建新的。当 entries_bound 到达上限的时候,开始 rebuild。<br />
** 当已有 entries 数组的大小在现有元素大小的 2 倍到 4 倍(REBUILD_THRESHOLD)之间,或者元素数量小于 16 个,进入压缩流程,直接使用原来 table 作为新 new_tab<br />
** 否则,进入新建 table 作为 new_tab<br />
** 小技巧 , prefetch 指令,预加载下个元素,在遍历 entries 的时候用到。<br />
<pre><br />
#define PREFETCH(addr, write_p) __builtin_prefetch(addr, write_p)<br />
PREFETCH(entries + i + 1, 0);<br />
</pre><br />
** rehash 其实很简单了,遍历已有的 entries 数组,跳过已经删除的,设置到新的 table 里,同时设置 bins:<br />
<pre><br />
bins = new_tab->bins;<br />
size_ind = get_size_ind(new_tab);<br />
for (i = tab->entries_start; i < bound; i++) {<br />
curr_entry_ptr = &entries[i];<br />
PREFETCH(entries + i + 1, 0);<br />
if (EXPECT(DELETED_ENTRY_P(curr_entry_ptr), 0))<br />
continue;<br />
if (&new_entries[ni] != curr_entry_ptr)<br />
new_entries[ni] = *curr_entry_ptr;<br />
if (EXPECT(bins != NULL, 1)) {<br />
bin_ind = find_table_bin_ind_direct(new_tab, curr_entry_ptr->hash,<br />
curr_entry_ptr->key);<br />
st_assert(bin_ind != UNDEFINED_BIN_IND<br />
&& (tab == new_tab || new_tab->rebuilds_num == 0)<br />
&& IND_EMPTY_BIN_P(new_tab, bin_ind));<br />
set_bin(bins, size_ind, bin_ind, ni + ENTRY_BASE);<br />
}<br />
new_tab->num_entries++;<br />
ni++;<br />
}<br />
<br />
</pre><br />
<br />
* 开放地址法,遇到哈希冲突,采用二次哈希,次级哈希函数如下:<br />
<pre><br />
/* Return the next secondary hash index for table TAB using previous<br />
index IND and PERTERB. Finally modulo of the function becomes a<br />
full *cycle linear congruential generator*, in other words it<br />
guarantees traversing all table bins in extreme case.<br />
<br />
According the Hull-Dobell theorem a generator<br />
"Xnext = (a*Xprev + c) mod m" is a full cycle generator iff<br />
o m and c are relatively prime<br />
o a-1 is divisible by all prime factors of m<br />
o a-1 is divisible by 4 if m is divisible by 4.<br />
<br />
For our case a is 5, c is 1, and m is a power of two. */<br />
static inline st_index_t<br />
secondary_hash(st_index_t ind, st_table *tab, st_index_t *perterb)<br />
{<br />
*perterb >>= 11;<br />
ind = (ind << 2) + ind + *perterb + 1;<br />
return hash_bin(ind, tab);<br />
}<br />
<br />
</pre><br />
<br />
* find_entry 线性探测,find_table_entry_ind 是二次哈希查找,但是可以选择是否启用二次探测:<br />
<pre><br />
/* Use the quadratic probing. The method has a better data locality<br />
but more collisions than the current approach. In average it<br />
results in a bit slower search. */<br />
/*#define QUADRATIC_PROBE*/<br />
<br />
ind = hash_bin(hash_value, tab);<br />
#ifdef QUADRATIC_PROBE<br />
d = 1;<br />
#else<br />
peterb = hash_value;<br />
#endif<br />
FOUND_BIN;<br />
for (;;) {<br />
bin = get_bin(tab->bins, get_size_ind(tab), ind);<br />
//找到相等的。<br />
if (! EMPTY_OR_DELETED_BIN_P(bin)<br />
&& PTR_EQUAL(tab, &entries[bin - ENTRY_BASE], hash_value, key))<br />
break;<br />
//或者找到空的。<br />
else if (EMPTY_BIN_P(bin))<br />
return UNDEFINED_ENTRY_IND;<br />
#ifdef QUADRATIC_PROBE<br />
//启用了二次探测,计算下一个探测位置。<br />
ind = hash_bin(ind + d, tab);<br />
d++;<br />
#else<br />
//或者二次哈希<br />
ind = secondary_hash(ind, tab, &peterb);<br />
#endif<br />
COLLISION;<br />
}<br />
return bin;<br />
</pre><br />
<br />
默认采用二次哈希。<br />
<br />
* 查找过程简单明了:<br />
<br />
<pre><br />
/* Find an entry with KEY in table TAB. Return non-zero if we found<br />
it. Set up *RECORD to the found entry record. */<br />
int<br />
st_lookup(st_table *tab, st_data_t key, st_data_t *value)<br />
{<br />
st_index_t bin;<br />
//计算哈希<br />
st_hash_t hash = do_hash(key, tab);<br />
<br />
if (tab->bins == NULL) {<br />
//线性查找,对于少于等于 16 个元素的table<br />
bin = find_entry(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
} else {<br />
//进入哈希查找。<br />
bin = find_table_entry_ind(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
bin -= ENTRY_BASE;<br />
}<br />
//赋值,返回<br />
if (value != 0)<br />
*value = tab->entries[bin].record;<br />
return 1;<br />
}<br />
<br />
</pre><br />
<br />
* 删除是标记删除,开放地址法必须这么做,否则冲突的时候,前面的删除,就找不到后面的元素了:<br />
<pre><br />
bin = get_bin(tab->bins, get_size_ind(tab), bin_ind) - ENTRY_BASE;<br />
MARK_BIN_DELETED(tab, bin_ind);<br />
}<br />
entry = &tab->entries[bin];<br />
*key = entry->key;<br />
if (value != 0) *value = entry->record;<br />
MARK_ENTRY_DELETED(entry);<br />
</pre><br />
<br />
== 闭包 ==<br />
* rb_block_struct 和 rb_control_frame_struct 两个结构,闭包本质是代码块和它的上下文环境(EP指针)。rb_block_t 结构体是 rb_control_frame_t 的一部分,避免分配。但是在最新代码里已经修改了, rb_struct 变成一个 union,其中 rb_captured_block 是 rb_control_frame_struct 一部分 :<br />
<br />
<pre><br />
typedef struct rb_control_frame_struct {<br />
const VALUE *pc; /* cfp[0] */<br />
VALUE *sp; /* cfp[1] */<br />
const rb_iseq_t *iseq; /* cfp[2] */<br />
VALUE self; /* cfp[3] / block[0] */<br />
const VALUE *ep; /* cfp[4] / block[1] */<br />
const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */<br />
<br />
#if VM_DEBUG_BP_CHECK<br />
VALUE *bp_check; /* cfp[6] */<br />
#endif<br />
} rb_control_frame_t;<br />
<br />
<br />
enum rb_block_type {<br />
block_type_iseq,<br />
block_type_ifunc,<br />
block_type_symbol,<br />
block_type_proc<br />
};<br />
<br />
struct rb_block {<br />
union {<br />
struct rb_captured_block captured;<br />
VALUE symbol;<br />
VALUE proc;<br />
} as;<br />
enum rb_block_type type;<br />
};<br />
<br />
typedef struct rb_control_frame_struct {<br />
const VALUE *pc; /* cfp[0] */<br />
VALUE *sp; /* cfp[1] */<br />
const rb_iseq_t *iseq; /* cfp[2] */<br />
VALUE self; /* cfp[3] / block[0] */<br />
const VALUE *ep; /* cfp[4] / block[1] */<br />
const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */<br />
<br />
#if VM_DEBUG_BP_CHECK<br />
VALUE *bp_check; /* cfp[6] */<br />
#endif<br />
} rb_control_frame_t;<br />
<br />
</pre><br />
<br />
* 上述代码中比较有意思的是 rb_block_type 分成四种。<br />
* 从测试来看(2.4), each block 确实会比 while 循环慢(在我的机器上是 60%)。因为block 需要做更多的工作:创建栈帧、拷贝 EP 指针等。<br />
* 创建 proc 的时候,会复制一份当前的栈帧副本 rb_env_t,proc 和 lambda 本质上是一样的,通过一个布尔值区分 is_lambda:<br />
<br />
<pre><br />
typedef struct {<br />
const struct rb_block block;<br />
int8_t safe_level; /* 0..1 */<br />
int8_t is_from_method; /* bool */<br />
int8_t is_lambda; /* bool */<br />
} rb_proc_t;<br />
<br />
typedef struct {<br />
VALUE flags; /* imemo header */<br />
const rb_iseq_t *iseq;<br />
const VALUE *ep;<br />
const VALUE *env;<br />
unsigned int env_size;<br />
} rb_env_t;<br />
<br />
</pre><br />
<br />
* proc 的创建是通过 RTypedData 和 rb_proc_t 结合来创建的:<br />
<br />
<pre><br />
struct RTypedData {<br />
struct RBasic basic;<br />
const rb_data_type_t *type;<br />
VALUE typed_flag; /* 1 or not */<br />
void *data;<br />
};<br />
<br />
/* Proc */<br />
<br />
VALUE<br />
rb_proc_create_from_captured(VALUE klass,<br />
const struct rb_captured_block *captured,<br />
enum rb_block_type block_type,<br />
int8_t safe_level, int8_t is_from_method, int8_t is_lambda)<br />
{<br />
VALUE procval = rb_proc_alloc(klass);<br />
rb_proc_t *proc = RTYPEDDATA_DATA(procval);<br />
<br />
VM_ASSERT(VM_EP_IN_HEAP_P(GET_THREAD(), captured->ep));<br />
<br />
/* copy block */<br />
RB_OBJ_WRITE(procval, &proc->block.as.captured.self, captured->self);<br />
RB_OBJ_WRITE(procval, &proc->block.as.captured.code.val, captured->code.val);<br />
*((const VALUE **)&proc->block.as.captured.ep) = captured->ep;<br />
RB_OBJ_WRITTEN(procval, Qundef, VM_ENV_ENVVAL(captured->ep));<br />
<br />
vm_block_type_set(&proc->block, block_type);<br />
proc->safe_level = safe_level;<br />
proc->is_from_method = is_from_method;<br />
proc->is_lambda = is_lambda;<br />
<br />
return procval;<br />
}<br />
</pre><br />
<br />
tagged struct 也是常见的 c 语言技巧。<br />
<br />
== 元编程 ==<br />
* class 关键字,开启一个新的词法作用域。<br />
* class 里 def 定义方法,三个步骤:编译代码指令、使用当前的词法作用域获取类或者模块的指针、在该类的方法表中保存新的方法。<br />
* 类方法 def self.xxx 是定义在类的元类里,也就是 Quote.singleton_class。<br />
* class << 类, 也是在元类定义方法,比之 self.xxx 主要是方便,<< 开启了一个新的词法作用域。<br />
* def 对象.xxx,会为对象创建新的单类(singleton class),并为它指派新的方法。<br />
* class << 对象与 def 对象.xxx 类似。<br />
* Ruby 2.0 引入 refine ,可以在某个模块中不修改原始类的情况下重新定义它的方法:<br />
<br />
<pre><br />
class Quote<br />
def display<br />
puts "The quick brown fox jumped over the lazy dog."<br />
end<br />
end<br />
<br />
module AllCaps<br />
refine Quote do<br />
def display<br />
puts "THE QUICK BROWN FOX JUMPED OVER THE LAZY DOG."<br />
end<br />
end<br />
end<br />
def test<br />
Quote.new.display<br />
using AllCaps<br />
Quote.new.display<br />
end<br />
<br />
Quote.new.display<br />
using AllCaps<br />
Quote.new.display<br />
<br />
test<br />
<br />
</pre><br />
<br />
using 暂时不允许使用在顶级作用域。using 相当于在当前作用域激活了 refinement 模块,方法查找将从当前激活的 refinement 模块开始,没有找到才去调用原始方法。<br />
<br />
== 垃圾回收 ==<br />
<br />
* MRI 使用的标记清除算法,在标记和清除的时候会暂停程序,在 1.9.3 开始引入了延迟清除(lazy sweep)的优化,降低垃圾回收每次带来的暂停时间,但是并不会减少整个垃圾回收的工作量。<br />
* 可以通过 GC.start 来强制发起 full gc<br />
* GC::Profiler.enable 和 GC::Profiler.report 提供了 GC 报告,总体上来说 gc 消耗的时间跟堆的大小成线性关系。<br />
* JRuby 的 gc 就是 JVM 的 gc,比如复制收集、分代收集、并发收集等等,不再重复。</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-04-02T06:33:39Z
<p>Dennis zhuang:/* 垃圾回收 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。参考 http://ju.outofmemory.cn/entry/135587<br />
<br />
* Ruby 的方法缓存包含两层:全局的方法缓存,用于缓存接收者和实现类之间的映射,因为方法查找是要遍历整个继承链的,缓存可以加速这个调用。其次是内联方法缓存,缓存 Ruby 已经执行的已编译的 YARV 指令信息,这样可以避免查找,加速的原理和 clojure 的 direct linking 技术是一样的。无论是定义新方法、include 模块或者其他类似的操作, Ruby 都会去清空这两个缓冲。<br />
* 多次include 不同模块,最近 include 的模块作为直接超类向上延伸。<br />
* 模块也可以 include 模块,规则与类 include 模块一致,也是副本插入作为超类,作为目标类和原始超类之间新的超类。<br />
* Module prepend 例子:<br />
<br />
<pre><br />
module Professor<br />
def name<br />
"Prof. #{super}"<br />
end<br />
end<br />
class Mathematician<br />
attr_accessor :name<br />
prepend Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.name = 'Johann Carl Friedrich Gauss'<br />
<br />
p p.name<br />
<br />
</pre><br />
<br />
* prepend 虽然仍然会将 Professor 设置为 Mathematician 的新超类,但是同时会拷贝一份 Mathematician 作为 Mathematician 原生类(Origin class),将这个原生类作为 Professor 的超类,这就可以解释为什么 Professor#name 的 super 能调用到 Mathematician 的 name 方法。参考 http://ju.outofmemory.cn/entry/135588<br />
* 修改已被 include 模块,比如增加方法,所有 include 该模块的类都将包含新方法,因为共享 m_tbl 方法表,Ruby 在 include 的时候拷贝的只是 RClass struct,不拷贝底层的方法表 ,看下面例子:<br />
<br />
<pre><br />
module Professor<br />
def letcures ; end<br />
end<br />
<br />
class Mathematician<br />
attr_accessor :first_name<br />
attr_accessor :last_name<br />
<br />
include Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.first_name = 'hello'<br />
p.last_name = 'world'<br />
<br />
p p.methods.sort<br />
<br />
#open Professor, adds new method<br />
module Professor<br />
def classroom; end<br />
end<br />
<br />
p p.methods.sort<br />
<br />
</pre><br />
<br />
* 但是修改已被 include 模块中 include 的其他模块,不会影响插入到 include 类的已经被拷贝的模块副本,也就不会增加或者删除方法。<br />
* 当创建一个 class或者模块的时候 ,其实是新建了一层词法作用域,Ruby 用两个指针来标示: nd_clss ,指向当前作用域对应的模块或者类;nd_next 指向父层或者上下文的词法作用域。形成一个作用域链条。<br />
* 常量的查找跟方法的查找类似,只是方法的查找是通过祖先连(super) 来查找,而常量是通过迭代词法作用域链(nd_next)来查找。<br />
* Ruby 优先通过词法作用域来查找常量:<br />
<br />
<pre><br />
class SuperClass<br />
FIND_ME = "Found in Superclass"<br />
end<br />
<br />
module ParentLexicalScope<br />
FIND_ME = "Found in ParentLexicalScope"<br />
<br />
module ChildLexicalScope<br />
<br />
class SubClass < SuperClass<br />
p FIND_ME<br />
end<br />
end<br />
end<br />
<br />
</pre><br />
<br />
输出 "Found in ParentLexicalScope"<br />
<br />
* 真实的 Ruby 常量查找还需要加入 autoload 关键字:<br />
<br />
『检索词法作用域链 -> 为了个作用域的类检查 autoload -> 检索超类链 -> 为每个超类检查 autoload -> 调用 const_missing。』。<br />
<br />
* 关于 autoload http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html<br />
<br />
== 散列表 ==<br />
<br />
* ruby 的散列表 hash 解决哈希冲突还是经典的链表法,密度阈值设定为 5,超过就做 rehash。也就是 java hash 的所谓负载因子。<br />
* rehash 扩容的大小不是翻倍之类的算法,而是基于素数,总是将容器数目设置为一个素数表里的素数大小。这个考虑也是基于对哈希函数不够分布的担忧。<br />
* 比较元素通过 eq? 方法。<br />
* 默认哈希算法采用 murmur hash,这跟 clojure 是一样的。自定义对象作为 key,同样也可以选择自定义实现 hash 函数。一般都推荐使用默认。<br />
* Ruby 2.0 做了个优化,跟 clojure 一样,小于等于 6 个元素的 hash 直接组织成一个数组, clojure 里是少于等于8个就是 PersistentArrayMap,节省内存和提升效率。<br />
* RHash 转移到 internal.h<br />
<br />
<pre><br />
struct RHash {<br />
struct RBasic basic;<br />
struct st_table *ntbl; /* possibly 0 */<br />
int iter_lev;<br />
const VALUE ifnone;<br />
};<br />
</pre><br />
<br />
* ruby 2.4 又有一个大的变化,使用开放地址法替换了链表法,参考 https://bugs.ruby-lang.org/issues/12142,这个讨论非常有价值。<br />
<br />
<pre><br />
/* The original package implemented classic bucket-based hash tables<br />
with entries doubly linked for an access by their insertion order.<br />
To decrease pointer chasing and as a consequence to improve a data<br />
locality the current implementation is based on storing entries in<br />
an array and using hash tables with open addressing. The current<br />
entries are more compact in comparison with the original ones and<br />
this also improves the data locality.<br />
<br />
The hash table has two arrays called *bins* and *entries*.<br />
<br />
bins:<br />
-------<br />
| | entries array:<br />
|-------| --------------------------------<br />
| index | | | entry: | | |<br />
|-------| | | | | |<br />
| ... | | ... | hash | ... | ... |<br />
|-------| | | key | | |<br />
| empty | | | record | | |<br />
|-------| --------------------------------<br />
| ... | ^ ^<br />
|-------| |_ entries start |_ entries bound<br />
|deleted|<br />
-------<br />
<br />
o The entry array contains table entries in the same order as they<br />
were inserted.<br />
<br />
When the first entry is deleted, a variable containing index of<br />
the current first entry (*entries start*) is changed. In all<br />
other cases of the deletion, we just mark the entry as deleted by<br />
using a reserved hash value.<br />
<br />
Such organization of the entry storage makes operations of the<br />
table shift and the entries traversal very fast.<br />
<br />
o The bins provide access to the entries by their keys. The<br />
key hash is mapped to a bin containing *index* of the<br />
corresponding entry in the entry array.<br />
<br />
The bin array size is always power of two, it makes mapping very<br />
fast by using the corresponding lower bits of the hash.<br />
Generally it is not a good idea to ignore some part of the hash.<br />
But alternative approach is worse. For example, we could use a<br />
modulo operation for mapping and a prime number for the size of<br />
the bin array. Unfortunately, the modulo operation for big<br />
64-bit numbers are extremely slow (it takes more than 100 cycles<br />
on modern Intel CPUs).<br />
<br />
Still other bits of the hash value are used when the mapping<br />
results in a collision. In this case we use a secondary hash<br />
value which is a result of a function of the collision bin<br />
index and the original hash value. The function choice<br />
guarantees that we can traverse all bins and finally find the<br />
corresponding bin as after several iterations the function<br />
becomes a full cycle linear congruential generator because it<br />
satisfies requirements of the Hull-Dobell theorem.<br />
<br />
When an entry is removed from the table besides marking the<br />
hash in the corresponding entry described above, we also mark<br />
the bin by a special value in order to find entries which had<br />
a collision with the removed entries.<br />
<br />
There are two reserved values for the bins. One denotes an<br />
empty bin, another one denotes a bin for a deleted entry.<br />
<br />
o The length of the bin array is at least two times more than the<br />
entry array length. This keeps the table load factor healthy.<br />
The trigger of rebuilding the table is always a case when we can<br />
not insert an entry anymore at the entries bound. We could<br />
change the entries bound too in case of deletion but than we need<br />
a special code to count bins with corresponding deleted entries<br />
and reset the bin values when there are too many bins<br />
corresponding deleted entries<br />
<br />
Table rebuilding is done by creation of a new entry array and<br />
bins of an appropriate size. We also try to reuse the arrays<br />
in some cases by compacting the array and removing deleted<br />
entries.<br />
<br />
o To save memory very small tables have no allocated arrays<br />
bins. We use a linear search for an access by a key.<br />
<br />
o To save more memory we use 8-, 16-, 32- and 64- bit indexes in<br />
bins depending on the current hash table size.<br />
<br />
This implementation speeds up the Ruby hash table benchmarks in<br />
average by more 40% on Intel Haswell CPU.<br />
<br />
*/<br />
<br />
</pre><br />
<br />
=== 2.4 st_table 阅读心得 ===<br />
* st_features 定义了 table 的属性:<br />
** entry_power entries数组的大小,2的幂指数。<br />
** bin_power bins数组大小,同样是2的幂指数<br />
** size_ind 根据 table 大小,选择 bins 对应的元素的大小,可能是 8-bit, 16-bit etc。<br />
** bins_words bins按照word计算的大小。<br />
** 根据 SIZEOF_ST_INDEX_T 枚举了一堆 table 属性。<br />
* 因为 bins 的大小都是 2 的次幂,因此计算哈希值对应的 bin 可以直接用位运算:<br />
<pre><br />
/* Return mask for a bin index in table TAB. */<br />
static inline st_index_t<br />
bins_mask(const st_table *tab)<br />
{<br />
return get_bins_num(tab) - 1;<br />
}<br />
<br />
/* Return the index of table TAB bin corresponding to<br />
HASH_VALUE. */<br />
static inline st_index_t<br />
hash_bin(st_hash_t hash_value, st_table *tab)<br />
{<br />
return hash_value & bins_mask(tab);<br />
}<br />
</pre><br />
* 最小的 table 大小是 4, 由 MINIMAL_POWER2 决定。最大的 table 大小是 2 的 30 次方(非 8 位平台),8 位平台上是 2 的 62 次方。<br />
* 对于小于等于 16 个元素的 table,不创建 bins 数组,直接存储在 entries 数组,线性探测,无需进行 hash 计算和查找。<br />
* rebuild_table 可能有两种: compact 现有的,或者创建新的。当 entries_bound 到达上限的时候,开始 rebuild。<br />
** 当已有 entries 数组的大小在现有元素大小的 2 倍到 4 倍(REBUILD_THRESHOLD)之间,或者元素数量小于 16 个,进入压缩流程,直接使用原来 table 作为新 new_tab<br />
** 否则,进入新建 table 作为 new_tab<br />
** 小技巧 , prefetch 指令,预加载下个元素,在遍历 entries 的时候用到。<br />
<pre><br />
#define PREFETCH(addr, write_p) __builtin_prefetch(addr, write_p)<br />
PREFETCH(entries + i + 1, 0);<br />
</pre><br />
** rehash 其实很简单了,遍历已有的 entries 数组,跳过已经删除的,设置到新的 table 里,同时设置 bins:<br />
<pre><br />
bins = new_tab->bins;<br />
size_ind = get_size_ind(new_tab);<br />
for (i = tab->entries_start; i < bound; i++) {<br />
curr_entry_ptr = &entries[i];<br />
PREFETCH(entries + i + 1, 0);<br />
if (EXPECT(DELETED_ENTRY_P(curr_entry_ptr), 0))<br />
continue;<br />
if (&new_entries[ni] != curr_entry_ptr)<br />
new_entries[ni] = *curr_entry_ptr;<br />
if (EXPECT(bins != NULL, 1)) {<br />
bin_ind = find_table_bin_ind_direct(new_tab, curr_entry_ptr->hash,<br />
curr_entry_ptr->key);<br />
st_assert(bin_ind != UNDEFINED_BIN_IND<br />
&& (tab == new_tab || new_tab->rebuilds_num == 0)<br />
&& IND_EMPTY_BIN_P(new_tab, bin_ind));<br />
set_bin(bins, size_ind, bin_ind, ni + ENTRY_BASE);<br />
}<br />
new_tab->num_entries++;<br />
ni++;<br />
}<br />
<br />
</pre><br />
<br />
* 开放地址法,遇到哈希冲突,采用二次哈希,次级哈希函数如下:<br />
<pre><br />
/* Return the next secondary hash index for table TAB using previous<br />
index IND and PERTERB. Finally modulo of the function becomes a<br />
full *cycle linear congruential generator*, in other words it<br />
guarantees traversing all table bins in extreme case.<br />
<br />
According the Hull-Dobell theorem a generator<br />
"Xnext = (a*Xprev + c) mod m" is a full cycle generator iff<br />
o m and c are relatively prime<br />
o a-1 is divisible by all prime factors of m<br />
o a-1 is divisible by 4 if m is divisible by 4.<br />
<br />
For our case a is 5, c is 1, and m is a power of two. */<br />
static inline st_index_t<br />
secondary_hash(st_index_t ind, st_table *tab, st_index_t *perterb)<br />
{<br />
*perterb >>= 11;<br />
ind = (ind << 2) + ind + *perterb + 1;<br />
return hash_bin(ind, tab);<br />
}<br />
<br />
</pre><br />
<br />
* find_entry 线性探测,find_table_entry_ind 是二次哈希查找,但是可以选择是否启用二次探测:<br />
<pre><br />
/* Use the quadratic probing. The method has a better data locality<br />
but more collisions than the current approach. In average it<br />
results in a bit slower search. */<br />
/*#define QUADRATIC_PROBE*/<br />
<br />
ind = hash_bin(hash_value, tab);<br />
#ifdef QUADRATIC_PROBE<br />
d = 1;<br />
#else<br />
peterb = hash_value;<br />
#endif<br />
FOUND_BIN;<br />
for (;;) {<br />
bin = get_bin(tab->bins, get_size_ind(tab), ind);<br />
//找到相等的。<br />
if (! EMPTY_OR_DELETED_BIN_P(bin)<br />
&& PTR_EQUAL(tab, &entries[bin - ENTRY_BASE], hash_value, key))<br />
break;<br />
//或者找到空的。<br />
else if (EMPTY_BIN_P(bin))<br />
return UNDEFINED_ENTRY_IND;<br />
#ifdef QUADRATIC_PROBE<br />
//启用了二次探测,计算下一个探测位置。<br />
ind = hash_bin(ind + d, tab);<br />
d++;<br />
#else<br />
//或者二次哈希<br />
ind = secondary_hash(ind, tab, &peterb);<br />
#endif<br />
COLLISION;<br />
}<br />
return bin;<br />
</pre><br />
<br />
默认采用二次哈希。<br />
<br />
* 查找过程简单明了:<br />
<br />
<pre><br />
/* Find an entry with KEY in table TAB. Return non-zero if we found<br />
it. Set up *RECORD to the found entry record. */<br />
int<br />
st_lookup(st_table *tab, st_data_t key, st_data_t *value)<br />
{<br />
st_index_t bin;<br />
//计算哈希<br />
st_hash_t hash = do_hash(key, tab);<br />
<br />
if (tab->bins == NULL) {<br />
//线性查找,对于少于等于 16 个元素的table<br />
bin = find_entry(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
} else {<br />
//进入哈希查找。<br />
bin = find_table_entry_ind(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
bin -= ENTRY_BASE;<br />
}<br />
//赋值,返回<br />
if (value != 0)<br />
*value = tab->entries[bin].record;<br />
return 1;<br />
}<br />
<br />
</pre><br />
<br />
* 删除是标记删除,开放地址法必须这么做,否则冲突的时候,前面的删除,就找不到后面的元素了:<br />
<pre><br />
bin = get_bin(tab->bins, get_size_ind(tab), bin_ind) - ENTRY_BASE;<br />
MARK_BIN_DELETED(tab, bin_ind);<br />
}<br />
entry = &tab->entries[bin];<br />
*key = entry->key;<br />
if (value != 0) *value = entry->record;<br />
MARK_ENTRY_DELETED(entry);<br />
</pre><br />
<br />
== 闭包 ==<br />
* rb_block_struct 和 rb_control_frame_struct 两个结构,闭包本质是代码块和它的上下文环境(EP指针)。rb_block_t 结构体是 rb_control_frame_t 的一部分,避免分配。但是在最新代码里已经修改了, rb_struct 变成一个 union,其中 rb_captured_block 是 rb_control_frame_struct 一部分 :<br />
<br />
<pre><br />
typedef struct rb_control_frame_struct {<br />
const VALUE *pc; /* cfp[0] */<br />
VALUE *sp; /* cfp[1] */<br />
const rb_iseq_t *iseq; /* cfp[2] */<br />
VALUE self; /* cfp[3] / block[0] */<br />
const VALUE *ep; /* cfp[4] / block[1] */<br />
const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */<br />
<br />
#if VM_DEBUG_BP_CHECK<br />
VALUE *bp_check; /* cfp[6] */<br />
#endif<br />
} rb_control_frame_t;<br />
<br />
<br />
enum rb_block_type {<br />
block_type_iseq,<br />
block_type_ifunc,<br />
block_type_symbol,<br />
block_type_proc<br />
};<br />
<br />
struct rb_block {<br />
union {<br />
struct rb_captured_block captured;<br />
VALUE symbol;<br />
VALUE proc;<br />
} as;<br />
enum rb_block_type type;<br />
};<br />
<br />
typedef struct rb_control_frame_struct {<br />
const VALUE *pc; /* cfp[0] */<br />
VALUE *sp; /* cfp[1] */<br />
const rb_iseq_t *iseq; /* cfp[2] */<br />
VALUE self; /* cfp[3] / block[0] */<br />
const VALUE *ep; /* cfp[4] / block[1] */<br />
const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */<br />
<br />
#if VM_DEBUG_BP_CHECK<br />
VALUE *bp_check; /* cfp[6] */<br />
#endif<br />
} rb_control_frame_t;<br />
<br />
</pre><br />
<br />
* 上述代码中比较有意思的是 rb_block_type 分成四种。<br />
* 从测试来看(2.4), each block 确实会比 while 循环慢(在我的机器上是 60%)。因为block 需要做更多的工作:创建栈帧、拷贝 EP 指针等。<br />
* 创建 proc 的时候,会复制一份当前的栈帧副本 rb_env_t,proc 和 lambda 本质上是一样的,通过一个布尔值区分 is_lambda:<br />
<br />
<pre><br />
typedef struct {<br />
const struct rb_block block;<br />
int8_t safe_level; /* 0..1 */<br />
int8_t is_from_method; /* bool */<br />
int8_t is_lambda; /* bool */<br />
} rb_proc_t;<br />
<br />
typedef struct {<br />
VALUE flags; /* imemo header */<br />
const rb_iseq_t *iseq;<br />
const VALUE *ep;<br />
const VALUE *env;<br />
unsigned int env_size;<br />
} rb_env_t;<br />
<br />
</pre><br />
<br />
* proc 的创建是通过 RTypedData 和 rb_proc_t 结合来创建的:<br />
<br />
<pre><br />
struct RTypedData {<br />
struct RBasic basic;<br />
const rb_data_type_t *type;<br />
VALUE typed_flag; /* 1 or not */<br />
void *data;<br />
};<br />
<br />
/* Proc */<br />
<br />
VALUE<br />
rb_proc_create_from_captured(VALUE klass,<br />
const struct rb_captured_block *captured,<br />
enum rb_block_type block_type,<br />
int8_t safe_level, int8_t is_from_method, int8_t is_lambda)<br />
{<br />
VALUE procval = rb_proc_alloc(klass);<br />
rb_proc_t *proc = RTYPEDDATA_DATA(procval);<br />
<br />
VM_ASSERT(VM_EP_IN_HEAP_P(GET_THREAD(), captured->ep));<br />
<br />
/* copy block */<br />
RB_OBJ_WRITE(procval, &proc->block.as.captured.self, captured->self);<br />
RB_OBJ_WRITE(procval, &proc->block.as.captured.code.val, captured->code.val);<br />
*((const VALUE **)&proc->block.as.captured.ep) = captured->ep;<br />
RB_OBJ_WRITTEN(procval, Qundef, VM_ENV_ENVVAL(captured->ep));<br />
<br />
vm_block_type_set(&proc->block, block_type);<br />
proc->safe_level = safe_level;<br />
proc->is_from_method = is_from_method;<br />
proc->is_lambda = is_lambda;<br />
<br />
return procval;<br />
}<br />
</pre><br />
<br />
tagged struct 也是常见的 c 语言技巧。<br />
<br />
== 元编程 ==<br />
* class 关键字,开启一个新的词法作用域。<br />
* class 里 def 定义方法,三个步骤:编译代码指令、使用当前的词法作用域获取类或者模块的指针、在该类的方法表中保存新的方法。<br />
* 类方法 def self.xxx 是定义在类的元类里,也就是 Quote.singleton_class。<br />
* class << 类, 也是在元类定义方法,比之 self.xxx 主要是方便,<< 开启了一个新的词法作用域。<br />
* def 对象.xxx,会为对象创建新的单类(singleton class),并为它指派新的方法。<br />
* class << 对象与 def 对象.xxx 类似。<br />
<br />
<br />
== 垃圾回收 ==<br />
<br />
* MRI 使用的标记清除算法,在标记和清除的时候会暂停程序,在 1.9.3 开始引入了延迟清除(lazy sweep)的优化,降低垃圾回收每次带来的暂停时间,但是并不会减少整个垃圾回收的工作量。<br />
* 可以通过 GC.start 来强制发起 full gc<br />
* GC::Profiler.enable 和 GC::Profiler.report 提供了 GC 报告,总体上来说 gc 消耗的时间跟堆的大小成线性关系。<br />
* JRuby 的 gc 就是 JVM 的 gc,比如复制收集、分代收集、并发收集等等,不再重复。</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-04-02T06:09:08Z
<p>Dennis zhuang:/* 闭包 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。参考 http://ju.outofmemory.cn/entry/135587<br />
<br />
* Ruby 的方法缓存包含两层:全局的方法缓存,用于缓存接收者和实现类之间的映射,因为方法查找是要遍历整个继承链的,缓存可以加速这个调用。其次是内联方法缓存,缓存 Ruby 已经执行的已编译的 YARV 指令信息,这样可以避免查找,加速的原理和 clojure 的 direct linking 技术是一样的。无论是定义新方法、include 模块或者其他类似的操作, Ruby 都会去清空这两个缓冲。<br />
* 多次include 不同模块,最近 include 的模块作为直接超类向上延伸。<br />
* 模块也可以 include 模块,规则与类 include 模块一致,也是副本插入作为超类,作为目标类和原始超类之间新的超类。<br />
* Module prepend 例子:<br />
<br />
<pre><br />
module Professor<br />
def name<br />
"Prof. #{super}"<br />
end<br />
end<br />
class Mathematician<br />
attr_accessor :name<br />
prepend Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.name = 'Johann Carl Friedrich Gauss'<br />
<br />
p p.name<br />
<br />
</pre><br />
<br />
* prepend 虽然仍然会将 Professor 设置为 Mathematician 的新超类,但是同时会拷贝一份 Mathematician 作为 Mathematician 原生类(Origin class),将这个原生类作为 Professor 的超类,这就可以解释为什么 Professor#name 的 super 能调用到 Mathematician 的 name 方法。参考 http://ju.outofmemory.cn/entry/135588<br />
* 修改已被 include 模块,比如增加方法,所有 include 该模块的类都将包含新方法,因为共享 m_tbl 方法表,Ruby 在 include 的时候拷贝的只是 RClass struct,不拷贝底层的方法表 ,看下面例子:<br />
<br />
<pre><br />
module Professor<br />
def letcures ; end<br />
end<br />
<br />
class Mathematician<br />
attr_accessor :first_name<br />
attr_accessor :last_name<br />
<br />
include Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.first_name = 'hello'<br />
p.last_name = 'world'<br />
<br />
p p.methods.sort<br />
<br />
#open Professor, adds new method<br />
module Professor<br />
def classroom; end<br />
end<br />
<br />
p p.methods.sort<br />
<br />
</pre><br />
<br />
* 但是修改已被 include 模块中 include 的其他模块,不会影响插入到 include 类的已经被拷贝的模块副本,也就不会增加或者删除方法。<br />
* 当创建一个 class或者模块的时候 ,其实是新建了一层词法作用域,Ruby 用两个指针来标示: nd_clss ,指向当前作用域对应的模块或者类;nd_next 指向父层或者上下文的词法作用域。形成一个作用域链条。<br />
* 常量的查找跟方法的查找类似,只是方法的查找是通过祖先连(super) 来查找,而常量是通过迭代词法作用域链(nd_next)来查找。<br />
* Ruby 优先通过词法作用域来查找常量:<br />
<br />
<pre><br />
class SuperClass<br />
FIND_ME = "Found in Superclass"<br />
end<br />
<br />
module ParentLexicalScope<br />
FIND_ME = "Found in ParentLexicalScope"<br />
<br />
module ChildLexicalScope<br />
<br />
class SubClass < SuperClass<br />
p FIND_ME<br />
end<br />
end<br />
end<br />
<br />
</pre><br />
<br />
输出 "Found in ParentLexicalScope"<br />
<br />
* 真实的 Ruby 常量查找还需要加入 autoload 关键字:<br />
<br />
『检索词法作用域链 -> 为了个作用域的类检查 autoload -> 检索超类链 -> 为每个超类检查 autoload -> 调用 const_missing。』。<br />
<br />
* 关于 autoload http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html<br />
<br />
== 散列表 ==<br />
<br />
* ruby 的散列表 hash 解决哈希冲突还是经典的链表法,密度阈值设定为 5,超过就做 rehash。也就是 java hash 的所谓负载因子。<br />
* rehash 扩容的大小不是翻倍之类的算法,而是基于素数,总是将容器数目设置为一个素数表里的素数大小。这个考虑也是基于对哈希函数不够分布的担忧。<br />
* 比较元素通过 eq? 方法。<br />
* 默认哈希算法采用 murmur hash,这跟 clojure 是一样的。自定义对象作为 key,同样也可以选择自定义实现 hash 函数。一般都推荐使用默认。<br />
* Ruby 2.0 做了个优化,跟 clojure 一样,小于等于 6 个元素的 hash 直接组织成一个数组, clojure 里是少于等于8个就是 PersistentArrayMap,节省内存和提升效率。<br />
* RHash 转移到 internal.h<br />
<br />
<pre><br />
struct RHash {<br />
struct RBasic basic;<br />
struct st_table *ntbl; /* possibly 0 */<br />
int iter_lev;<br />
const VALUE ifnone;<br />
};<br />
</pre><br />
<br />
* ruby 2.4 又有一个大的变化,使用开放地址法替换了链表法,参考 https://bugs.ruby-lang.org/issues/12142,这个讨论非常有价值。<br />
<br />
<pre><br />
/* The original package implemented classic bucket-based hash tables<br />
with entries doubly linked for an access by their insertion order.<br />
To decrease pointer chasing and as a consequence to improve a data<br />
locality the current implementation is based on storing entries in<br />
an array and using hash tables with open addressing. The current<br />
entries are more compact in comparison with the original ones and<br />
this also improves the data locality.<br />
<br />
The hash table has two arrays called *bins* and *entries*.<br />
<br />
bins:<br />
-------<br />
| | entries array:<br />
|-------| --------------------------------<br />
| index | | | entry: | | |<br />
|-------| | | | | |<br />
| ... | | ... | hash | ... | ... |<br />
|-------| | | key | | |<br />
| empty | | | record | | |<br />
|-------| --------------------------------<br />
| ... | ^ ^<br />
|-------| |_ entries start |_ entries bound<br />
|deleted|<br />
-------<br />
<br />
o The entry array contains table entries in the same order as they<br />
were inserted.<br />
<br />
When the first entry is deleted, a variable containing index of<br />
the current first entry (*entries start*) is changed. In all<br />
other cases of the deletion, we just mark the entry as deleted by<br />
using a reserved hash value.<br />
<br />
Such organization of the entry storage makes operations of the<br />
table shift and the entries traversal very fast.<br />
<br />
o The bins provide access to the entries by their keys. The<br />
key hash is mapped to a bin containing *index* of the<br />
corresponding entry in the entry array.<br />
<br />
The bin array size is always power of two, it makes mapping very<br />
fast by using the corresponding lower bits of the hash.<br />
Generally it is not a good idea to ignore some part of the hash.<br />
But alternative approach is worse. For example, we could use a<br />
modulo operation for mapping and a prime number for the size of<br />
the bin array. Unfortunately, the modulo operation for big<br />
64-bit numbers are extremely slow (it takes more than 100 cycles<br />
on modern Intel CPUs).<br />
<br />
Still other bits of the hash value are used when the mapping<br />
results in a collision. In this case we use a secondary hash<br />
value which is a result of a function of the collision bin<br />
index and the original hash value. The function choice<br />
guarantees that we can traverse all bins and finally find the<br />
corresponding bin as after several iterations the function<br />
becomes a full cycle linear congruential generator because it<br />
satisfies requirements of the Hull-Dobell theorem.<br />
<br />
When an entry is removed from the table besides marking the<br />
hash in the corresponding entry described above, we also mark<br />
the bin by a special value in order to find entries which had<br />
a collision with the removed entries.<br />
<br />
There are two reserved values for the bins. One denotes an<br />
empty bin, another one denotes a bin for a deleted entry.<br />
<br />
o The length of the bin array is at least two times more than the<br />
entry array length. This keeps the table load factor healthy.<br />
The trigger of rebuilding the table is always a case when we can<br />
not insert an entry anymore at the entries bound. We could<br />
change the entries bound too in case of deletion but than we need<br />
a special code to count bins with corresponding deleted entries<br />
and reset the bin values when there are too many bins<br />
corresponding deleted entries<br />
<br />
Table rebuilding is done by creation of a new entry array and<br />
bins of an appropriate size. We also try to reuse the arrays<br />
in some cases by compacting the array and removing deleted<br />
entries.<br />
<br />
o To save memory very small tables have no allocated arrays<br />
bins. We use a linear search for an access by a key.<br />
<br />
o To save more memory we use 8-, 16-, 32- and 64- bit indexes in<br />
bins depending on the current hash table size.<br />
<br />
This implementation speeds up the Ruby hash table benchmarks in<br />
average by more 40% on Intel Haswell CPU.<br />
<br />
*/<br />
<br />
</pre><br />
<br />
=== 2.4 st_table 阅读心得 ===<br />
* st_features 定义了 table 的属性:<br />
** entry_power entries数组的大小,2的幂指数。<br />
** bin_power bins数组大小,同样是2的幂指数<br />
** size_ind 根据 table 大小,选择 bins 对应的元素的大小,可能是 8-bit, 16-bit etc。<br />
** bins_words bins按照word计算的大小。<br />
** 根据 SIZEOF_ST_INDEX_T 枚举了一堆 table 属性。<br />
* 因为 bins 的大小都是 2 的次幂,因此计算哈希值对应的 bin 可以直接用位运算:<br />
<pre><br />
/* Return mask for a bin index in table TAB. */<br />
static inline st_index_t<br />
bins_mask(const st_table *tab)<br />
{<br />
return get_bins_num(tab) - 1;<br />
}<br />
<br />
/* Return the index of table TAB bin corresponding to<br />
HASH_VALUE. */<br />
static inline st_index_t<br />
hash_bin(st_hash_t hash_value, st_table *tab)<br />
{<br />
return hash_value & bins_mask(tab);<br />
}<br />
</pre><br />
* 最小的 table 大小是 4, 由 MINIMAL_POWER2 决定。最大的 table 大小是 2 的 30 次方(非 8 位平台),8 位平台上是 2 的 62 次方。<br />
* 对于小于等于 16 个元素的 table,不创建 bins 数组,直接存储在 entries 数组,线性探测,无需进行 hash 计算和查找。<br />
* rebuild_table 可能有两种: compact 现有的,或者创建新的。当 entries_bound 到达上限的时候,开始 rebuild。<br />
** 当已有 entries 数组的大小在现有元素大小的 2 倍到 4 倍(REBUILD_THRESHOLD)之间,或者元素数量小于 16 个,进入压缩流程,直接使用原来 table 作为新 new_tab<br />
** 否则,进入新建 table 作为 new_tab<br />
** 小技巧 , prefetch 指令,预加载下个元素,在遍历 entries 的时候用到。<br />
<pre><br />
#define PREFETCH(addr, write_p) __builtin_prefetch(addr, write_p)<br />
PREFETCH(entries + i + 1, 0);<br />
</pre><br />
** rehash 其实很简单了,遍历已有的 entries 数组,跳过已经删除的,设置到新的 table 里,同时设置 bins:<br />
<pre><br />
bins = new_tab->bins;<br />
size_ind = get_size_ind(new_tab);<br />
for (i = tab->entries_start; i < bound; i++) {<br />
curr_entry_ptr = &entries[i];<br />
PREFETCH(entries + i + 1, 0);<br />
if (EXPECT(DELETED_ENTRY_P(curr_entry_ptr), 0))<br />
continue;<br />
if (&new_entries[ni] != curr_entry_ptr)<br />
new_entries[ni] = *curr_entry_ptr;<br />
if (EXPECT(bins != NULL, 1)) {<br />
bin_ind = find_table_bin_ind_direct(new_tab, curr_entry_ptr->hash,<br />
curr_entry_ptr->key);<br />
st_assert(bin_ind != UNDEFINED_BIN_IND<br />
&& (tab == new_tab || new_tab->rebuilds_num == 0)<br />
&& IND_EMPTY_BIN_P(new_tab, bin_ind));<br />
set_bin(bins, size_ind, bin_ind, ni + ENTRY_BASE);<br />
}<br />
new_tab->num_entries++;<br />
ni++;<br />
}<br />
<br />
</pre><br />
<br />
* 开放地址法,遇到哈希冲突,采用二次哈希,次级哈希函数如下:<br />
<pre><br />
/* Return the next secondary hash index for table TAB using previous<br />
index IND and PERTERB. Finally modulo of the function becomes a<br />
full *cycle linear congruential generator*, in other words it<br />
guarantees traversing all table bins in extreme case.<br />
<br />
According the Hull-Dobell theorem a generator<br />
"Xnext = (a*Xprev + c) mod m" is a full cycle generator iff<br />
o m and c are relatively prime<br />
o a-1 is divisible by all prime factors of m<br />
o a-1 is divisible by 4 if m is divisible by 4.<br />
<br />
For our case a is 5, c is 1, and m is a power of two. */<br />
static inline st_index_t<br />
secondary_hash(st_index_t ind, st_table *tab, st_index_t *perterb)<br />
{<br />
*perterb >>= 11;<br />
ind = (ind << 2) + ind + *perterb + 1;<br />
return hash_bin(ind, tab);<br />
}<br />
<br />
</pre><br />
<br />
* find_entry 线性探测,find_table_entry_ind 是二次哈希查找,但是可以选择是否启用二次探测:<br />
<pre><br />
/* Use the quadratic probing. The method has a better data locality<br />
but more collisions than the current approach. In average it<br />
results in a bit slower search. */<br />
/*#define QUADRATIC_PROBE*/<br />
<br />
ind = hash_bin(hash_value, tab);<br />
#ifdef QUADRATIC_PROBE<br />
d = 1;<br />
#else<br />
peterb = hash_value;<br />
#endif<br />
FOUND_BIN;<br />
for (;;) {<br />
bin = get_bin(tab->bins, get_size_ind(tab), ind);<br />
//找到相等的。<br />
if (! EMPTY_OR_DELETED_BIN_P(bin)<br />
&& PTR_EQUAL(tab, &entries[bin - ENTRY_BASE], hash_value, key))<br />
break;<br />
//或者找到空的。<br />
else if (EMPTY_BIN_P(bin))<br />
return UNDEFINED_ENTRY_IND;<br />
#ifdef QUADRATIC_PROBE<br />
//启用了二次探测,计算下一个探测位置。<br />
ind = hash_bin(ind + d, tab);<br />
d++;<br />
#else<br />
//或者二次哈希<br />
ind = secondary_hash(ind, tab, &peterb);<br />
#endif<br />
COLLISION;<br />
}<br />
return bin;<br />
</pre><br />
<br />
默认采用二次哈希。<br />
<br />
* 查找过程简单明了:<br />
<br />
<pre><br />
/* Find an entry with KEY in table TAB. Return non-zero if we found<br />
it. Set up *RECORD to the found entry record. */<br />
int<br />
st_lookup(st_table *tab, st_data_t key, st_data_t *value)<br />
{<br />
st_index_t bin;<br />
//计算哈希<br />
st_hash_t hash = do_hash(key, tab);<br />
<br />
if (tab->bins == NULL) {<br />
//线性查找,对于少于等于 16 个元素的table<br />
bin = find_entry(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
} else {<br />
//进入哈希查找。<br />
bin = find_table_entry_ind(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
bin -= ENTRY_BASE;<br />
}<br />
//赋值,返回<br />
if (value != 0)<br />
*value = tab->entries[bin].record;<br />
return 1;<br />
}<br />
<br />
</pre><br />
<br />
* 删除是标记删除,开放地址法必须这么做,否则冲突的时候,前面的删除,就找不到后面的元素了:<br />
<pre><br />
bin = get_bin(tab->bins, get_size_ind(tab), bin_ind) - ENTRY_BASE;<br />
MARK_BIN_DELETED(tab, bin_ind);<br />
}<br />
entry = &tab->entries[bin];<br />
*key = entry->key;<br />
if (value != 0) *value = entry->record;<br />
MARK_ENTRY_DELETED(entry);<br />
</pre><br />
<br />
== 闭包 ==<br />
* rb_block_struct 和 rb_control_frame_struct 两个结构,闭包本质是代码块和它的上下文环境(EP指针)。rb_block_t 结构体是 rb_control_frame_t 的一部分,避免分配。但是在最新代码里已经修改了, rb_struct 变成一个 union,其中 rb_captured_block 是 rb_control_frame_struct 一部分 :<br />
<br />
<pre><br />
typedef struct rb_control_frame_struct {<br />
const VALUE *pc; /* cfp[0] */<br />
VALUE *sp; /* cfp[1] */<br />
const rb_iseq_t *iseq; /* cfp[2] */<br />
VALUE self; /* cfp[3] / block[0] */<br />
const VALUE *ep; /* cfp[4] / block[1] */<br />
const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */<br />
<br />
#if VM_DEBUG_BP_CHECK<br />
VALUE *bp_check; /* cfp[6] */<br />
#endif<br />
} rb_control_frame_t;<br />
<br />
<br />
enum rb_block_type {<br />
block_type_iseq,<br />
block_type_ifunc,<br />
block_type_symbol,<br />
block_type_proc<br />
};<br />
<br />
struct rb_block {<br />
union {<br />
struct rb_captured_block captured;<br />
VALUE symbol;<br />
VALUE proc;<br />
} as;<br />
enum rb_block_type type;<br />
};<br />
<br />
typedef struct rb_control_frame_struct {<br />
const VALUE *pc; /* cfp[0] */<br />
VALUE *sp; /* cfp[1] */<br />
const rb_iseq_t *iseq; /* cfp[2] */<br />
VALUE self; /* cfp[3] / block[0] */<br />
const VALUE *ep; /* cfp[4] / block[1] */<br />
const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */<br />
<br />
#if VM_DEBUG_BP_CHECK<br />
VALUE *bp_check; /* cfp[6] */<br />
#endif<br />
} rb_control_frame_t;<br />
<br />
</pre><br />
<br />
* 上述代码中比较有意思的是 rb_block_type 分成四种。<br />
* 从测试来看(2.4), each block 确实会比 while 循环慢(在我的机器上是 60%)。因为block 需要做更多的工作:创建栈帧、拷贝 EP 指针等。<br />
* 创建 proc 的时候,会复制一份当前的栈帧副本 rb_env_t,proc 和 lambda 本质上是一样的,通过一个布尔值区分 is_lambda:<br />
<br />
<pre><br />
typedef struct {<br />
const struct rb_block block;<br />
int8_t safe_level; /* 0..1 */<br />
int8_t is_from_method; /* bool */<br />
int8_t is_lambda; /* bool */<br />
} rb_proc_t;<br />
<br />
typedef struct {<br />
VALUE flags; /* imemo header */<br />
const rb_iseq_t *iseq;<br />
const VALUE *ep;<br />
const VALUE *env;<br />
unsigned int env_size;<br />
} rb_env_t;<br />
<br />
</pre><br />
<br />
* proc 的创建是通过 RTypedData 和 rb_proc_t 结合来创建的:<br />
<br />
<pre><br />
struct RTypedData {<br />
struct RBasic basic;<br />
const rb_data_type_t *type;<br />
VALUE typed_flag; /* 1 or not */<br />
void *data;<br />
};<br />
<br />
/* Proc */<br />
<br />
VALUE<br />
rb_proc_create_from_captured(VALUE klass,<br />
const struct rb_captured_block *captured,<br />
enum rb_block_type block_type,<br />
int8_t safe_level, int8_t is_from_method, int8_t is_lambda)<br />
{<br />
VALUE procval = rb_proc_alloc(klass);<br />
rb_proc_t *proc = RTYPEDDATA_DATA(procval);<br />
<br />
VM_ASSERT(VM_EP_IN_HEAP_P(GET_THREAD(), captured->ep));<br />
<br />
/* copy block */<br />
RB_OBJ_WRITE(procval, &proc->block.as.captured.self, captured->self);<br />
RB_OBJ_WRITE(procval, &proc->block.as.captured.code.val, captured->code.val);<br />
*((const VALUE **)&proc->block.as.captured.ep) = captured->ep;<br />
RB_OBJ_WRITTEN(procval, Qundef, VM_ENV_ENVVAL(captured->ep));<br />
<br />
vm_block_type_set(&proc->block, block_type);<br />
proc->safe_level = safe_level;<br />
proc->is_from_method = is_from_method;<br />
proc->is_lambda = is_lambda;<br />
<br />
return procval;<br />
}<br />
</pre><br />
<br />
tagged struct 也是常见的 c 语言技巧。<br />
<br />
== 垃圾回收 ==<br />
<br />
* MRI 使用的标记清除算法,在标记和清除的时候会暂停程序,在 1.9.3 开始引入了延迟清除(lazy sweep)的优化,降低垃圾回收每次带来的暂停时间,但是并不会减少整个垃圾回收的工作量。<br />
* 可以通过 GC.start 来强制发起 full gc<br />
* GC::Profiler.enable 和 GC::Profiler.report 提供了 GC 报告,总体上来说 gc 消耗的时间跟堆的大小成线性关系。<br />
* JRuby 的 gc 就是 JVM 的 gc,比如复制收集、分代收集、并发收集等等,不再重复。</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-04-02T05:51:28Z
<p>Dennis zhuang:/* 垃圾回收 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。参考 http://ju.outofmemory.cn/entry/135587<br />
<br />
* Ruby 的方法缓存包含两层:全局的方法缓存,用于缓存接收者和实现类之间的映射,因为方法查找是要遍历整个继承链的,缓存可以加速这个调用。其次是内联方法缓存,缓存 Ruby 已经执行的已编译的 YARV 指令信息,这样可以避免查找,加速的原理和 clojure 的 direct linking 技术是一样的。无论是定义新方法、include 模块或者其他类似的操作, Ruby 都会去清空这两个缓冲。<br />
* 多次include 不同模块,最近 include 的模块作为直接超类向上延伸。<br />
* 模块也可以 include 模块,规则与类 include 模块一致,也是副本插入作为超类,作为目标类和原始超类之间新的超类。<br />
* Module prepend 例子:<br />
<br />
<pre><br />
module Professor<br />
def name<br />
"Prof. #{super}"<br />
end<br />
end<br />
class Mathematician<br />
attr_accessor :name<br />
prepend Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.name = 'Johann Carl Friedrich Gauss'<br />
<br />
p p.name<br />
<br />
</pre><br />
<br />
* prepend 虽然仍然会将 Professor 设置为 Mathematician 的新超类,但是同时会拷贝一份 Mathematician 作为 Mathematician 原生类(Origin class),将这个原生类作为 Professor 的超类,这就可以解释为什么 Professor#name 的 super 能调用到 Mathematician 的 name 方法。参考 http://ju.outofmemory.cn/entry/135588<br />
* 修改已被 include 模块,比如增加方法,所有 include 该模块的类都将包含新方法,因为共享 m_tbl 方法表,Ruby 在 include 的时候拷贝的只是 RClass struct,不拷贝底层的方法表 ,看下面例子:<br />
<br />
<pre><br />
module Professor<br />
def letcures ; end<br />
end<br />
<br />
class Mathematician<br />
attr_accessor :first_name<br />
attr_accessor :last_name<br />
<br />
include Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.first_name = 'hello'<br />
p.last_name = 'world'<br />
<br />
p p.methods.sort<br />
<br />
#open Professor, adds new method<br />
module Professor<br />
def classroom; end<br />
end<br />
<br />
p p.methods.sort<br />
<br />
</pre><br />
<br />
* 但是修改已被 include 模块中 include 的其他模块,不会影响插入到 include 类的已经被拷贝的模块副本,也就不会增加或者删除方法。<br />
* 当创建一个 class或者模块的时候 ,其实是新建了一层词法作用域,Ruby 用两个指针来标示: nd_clss ,指向当前作用域对应的模块或者类;nd_next 指向父层或者上下文的词法作用域。形成一个作用域链条。<br />
* 常量的查找跟方法的查找类似,只是方法的查找是通过祖先连(super) 来查找,而常量是通过迭代词法作用域链(nd_next)来查找。<br />
* Ruby 优先通过词法作用域来查找常量:<br />
<br />
<pre><br />
class SuperClass<br />
FIND_ME = "Found in Superclass"<br />
end<br />
<br />
module ParentLexicalScope<br />
FIND_ME = "Found in ParentLexicalScope"<br />
<br />
module ChildLexicalScope<br />
<br />
class SubClass < SuperClass<br />
p FIND_ME<br />
end<br />
end<br />
end<br />
<br />
</pre><br />
<br />
输出 "Found in ParentLexicalScope"<br />
<br />
* 真实的 Ruby 常量查找还需要加入 autoload 关键字:<br />
<br />
『检索词法作用域链 -> 为了个作用域的类检查 autoload -> 检索超类链 -> 为每个超类检查 autoload -> 调用 const_missing。』。<br />
<br />
* 关于 autoload http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html<br />
<br />
== 散列表 ==<br />
<br />
* ruby 的散列表 hash 解决哈希冲突还是经典的链表法,密度阈值设定为 5,超过就做 rehash。也就是 java hash 的所谓负载因子。<br />
* rehash 扩容的大小不是翻倍之类的算法,而是基于素数,总是将容器数目设置为一个素数表里的素数大小。这个考虑也是基于对哈希函数不够分布的担忧。<br />
* 比较元素通过 eq? 方法。<br />
* 默认哈希算法采用 murmur hash,这跟 clojure 是一样的。自定义对象作为 key,同样也可以选择自定义实现 hash 函数。一般都推荐使用默认。<br />
* Ruby 2.0 做了个优化,跟 clojure 一样,小于等于 6 个元素的 hash 直接组织成一个数组, clojure 里是少于等于8个就是 PersistentArrayMap,节省内存和提升效率。<br />
* RHash 转移到 internal.h<br />
<br />
<pre><br />
struct RHash {<br />
struct RBasic basic;<br />
struct st_table *ntbl; /* possibly 0 */<br />
int iter_lev;<br />
const VALUE ifnone;<br />
};<br />
</pre><br />
<br />
* ruby 2.4 又有一个大的变化,使用开放地址法替换了链表法,参考 https://bugs.ruby-lang.org/issues/12142,这个讨论非常有价值。<br />
<br />
<pre><br />
/* The original package implemented classic bucket-based hash tables<br />
with entries doubly linked for an access by their insertion order.<br />
To decrease pointer chasing and as a consequence to improve a data<br />
locality the current implementation is based on storing entries in<br />
an array and using hash tables with open addressing. The current<br />
entries are more compact in comparison with the original ones and<br />
this also improves the data locality.<br />
<br />
The hash table has two arrays called *bins* and *entries*.<br />
<br />
bins:<br />
-------<br />
| | entries array:<br />
|-------| --------------------------------<br />
| index | | | entry: | | |<br />
|-------| | | | | |<br />
| ... | | ... | hash | ... | ... |<br />
|-------| | | key | | |<br />
| empty | | | record | | |<br />
|-------| --------------------------------<br />
| ... | ^ ^<br />
|-------| |_ entries start |_ entries bound<br />
|deleted|<br />
-------<br />
<br />
o The entry array contains table entries in the same order as they<br />
were inserted.<br />
<br />
When the first entry is deleted, a variable containing index of<br />
the current first entry (*entries start*) is changed. In all<br />
other cases of the deletion, we just mark the entry as deleted by<br />
using a reserved hash value.<br />
<br />
Such organization of the entry storage makes operations of the<br />
table shift and the entries traversal very fast.<br />
<br />
o The bins provide access to the entries by their keys. The<br />
key hash is mapped to a bin containing *index* of the<br />
corresponding entry in the entry array.<br />
<br />
The bin array size is always power of two, it makes mapping very<br />
fast by using the corresponding lower bits of the hash.<br />
Generally it is not a good idea to ignore some part of the hash.<br />
But alternative approach is worse. For example, we could use a<br />
modulo operation for mapping and a prime number for the size of<br />
the bin array. Unfortunately, the modulo operation for big<br />
64-bit numbers are extremely slow (it takes more than 100 cycles<br />
on modern Intel CPUs).<br />
<br />
Still other bits of the hash value are used when the mapping<br />
results in a collision. In this case we use a secondary hash<br />
value which is a result of a function of the collision bin<br />
index and the original hash value. The function choice<br />
guarantees that we can traverse all bins and finally find the<br />
corresponding bin as after several iterations the function<br />
becomes a full cycle linear congruential generator because it<br />
satisfies requirements of the Hull-Dobell theorem.<br />
<br />
When an entry is removed from the table besides marking the<br />
hash in the corresponding entry described above, we also mark<br />
the bin by a special value in order to find entries which had<br />
a collision with the removed entries.<br />
<br />
There are two reserved values for the bins. One denotes an<br />
empty bin, another one denotes a bin for a deleted entry.<br />
<br />
o The length of the bin array is at least two times more than the<br />
entry array length. This keeps the table load factor healthy.<br />
The trigger of rebuilding the table is always a case when we can<br />
not insert an entry anymore at the entries bound. We could<br />
change the entries bound too in case of deletion but than we need<br />
a special code to count bins with corresponding deleted entries<br />
and reset the bin values when there are too many bins<br />
corresponding deleted entries<br />
<br />
Table rebuilding is done by creation of a new entry array and<br />
bins of an appropriate size. We also try to reuse the arrays<br />
in some cases by compacting the array and removing deleted<br />
entries.<br />
<br />
o To save memory very small tables have no allocated arrays<br />
bins. We use a linear search for an access by a key.<br />
<br />
o To save more memory we use 8-, 16-, 32- and 64- bit indexes in<br />
bins depending on the current hash table size.<br />
<br />
This implementation speeds up the Ruby hash table benchmarks in<br />
average by more 40% on Intel Haswell CPU.<br />
<br />
*/<br />
<br />
</pre><br />
<br />
=== 2.4 st_table 阅读心得 ===<br />
* st_features 定义了 table 的属性:<br />
** entry_power entries数组的大小,2的幂指数。<br />
** bin_power bins数组大小,同样是2的幂指数<br />
** size_ind 根据 table 大小,选择 bins 对应的元素的大小,可能是 8-bit, 16-bit etc。<br />
** bins_words bins按照word计算的大小。<br />
** 根据 SIZEOF_ST_INDEX_T 枚举了一堆 table 属性。<br />
* 因为 bins 的大小都是 2 的次幂,因此计算哈希值对应的 bin 可以直接用位运算:<br />
<pre><br />
/* Return mask for a bin index in table TAB. */<br />
static inline st_index_t<br />
bins_mask(const st_table *tab)<br />
{<br />
return get_bins_num(tab) - 1;<br />
}<br />
<br />
/* Return the index of table TAB bin corresponding to<br />
HASH_VALUE. */<br />
static inline st_index_t<br />
hash_bin(st_hash_t hash_value, st_table *tab)<br />
{<br />
return hash_value & bins_mask(tab);<br />
}<br />
</pre><br />
* 最小的 table 大小是 4, 由 MINIMAL_POWER2 决定。最大的 table 大小是 2 的 30 次方(非 8 位平台),8 位平台上是 2 的 62 次方。<br />
* 对于小于等于 16 个元素的 table,不创建 bins 数组,直接存储在 entries 数组,线性探测,无需进行 hash 计算和查找。<br />
* rebuild_table 可能有两种: compact 现有的,或者创建新的。当 entries_bound 到达上限的时候,开始 rebuild。<br />
** 当已有 entries 数组的大小在现有元素大小的 2 倍到 4 倍(REBUILD_THRESHOLD)之间,或者元素数量小于 16 个,进入压缩流程,直接使用原来 table 作为新 new_tab<br />
** 否则,进入新建 table 作为 new_tab<br />
** 小技巧 , prefetch 指令,预加载下个元素,在遍历 entries 的时候用到。<br />
<pre><br />
#define PREFETCH(addr, write_p) __builtin_prefetch(addr, write_p)<br />
PREFETCH(entries + i + 1, 0);<br />
</pre><br />
** rehash 其实很简单了,遍历已有的 entries 数组,跳过已经删除的,设置到新的 table 里,同时设置 bins:<br />
<pre><br />
bins = new_tab->bins;<br />
size_ind = get_size_ind(new_tab);<br />
for (i = tab->entries_start; i < bound; i++) {<br />
curr_entry_ptr = &entries[i];<br />
PREFETCH(entries + i + 1, 0);<br />
if (EXPECT(DELETED_ENTRY_P(curr_entry_ptr), 0))<br />
continue;<br />
if (&new_entries[ni] != curr_entry_ptr)<br />
new_entries[ni] = *curr_entry_ptr;<br />
if (EXPECT(bins != NULL, 1)) {<br />
bin_ind = find_table_bin_ind_direct(new_tab, curr_entry_ptr->hash,<br />
curr_entry_ptr->key);<br />
st_assert(bin_ind != UNDEFINED_BIN_IND<br />
&& (tab == new_tab || new_tab->rebuilds_num == 0)<br />
&& IND_EMPTY_BIN_P(new_tab, bin_ind));<br />
set_bin(bins, size_ind, bin_ind, ni + ENTRY_BASE);<br />
}<br />
new_tab->num_entries++;<br />
ni++;<br />
}<br />
<br />
</pre><br />
<br />
* 开放地址法,遇到哈希冲突,采用二次哈希,次级哈希函数如下:<br />
<pre><br />
/* Return the next secondary hash index for table TAB using previous<br />
index IND and PERTERB. Finally modulo of the function becomes a<br />
full *cycle linear congruential generator*, in other words it<br />
guarantees traversing all table bins in extreme case.<br />
<br />
According the Hull-Dobell theorem a generator<br />
"Xnext = (a*Xprev + c) mod m" is a full cycle generator iff<br />
o m and c are relatively prime<br />
o a-1 is divisible by all prime factors of m<br />
o a-1 is divisible by 4 if m is divisible by 4.<br />
<br />
For our case a is 5, c is 1, and m is a power of two. */<br />
static inline st_index_t<br />
secondary_hash(st_index_t ind, st_table *tab, st_index_t *perterb)<br />
{<br />
*perterb >>= 11;<br />
ind = (ind << 2) + ind + *perterb + 1;<br />
return hash_bin(ind, tab);<br />
}<br />
<br />
</pre><br />
<br />
* find_entry 线性探测,find_table_entry_ind 是二次哈希查找,但是可以选择是否启用二次探测:<br />
<pre><br />
/* Use the quadratic probing. The method has a better data locality<br />
but more collisions than the current approach. In average it<br />
results in a bit slower search. */<br />
/*#define QUADRATIC_PROBE*/<br />
<br />
ind = hash_bin(hash_value, tab);<br />
#ifdef QUADRATIC_PROBE<br />
d = 1;<br />
#else<br />
peterb = hash_value;<br />
#endif<br />
FOUND_BIN;<br />
for (;;) {<br />
bin = get_bin(tab->bins, get_size_ind(tab), ind);<br />
//找到相等的。<br />
if (! EMPTY_OR_DELETED_BIN_P(bin)<br />
&& PTR_EQUAL(tab, &entries[bin - ENTRY_BASE], hash_value, key))<br />
break;<br />
//或者找到空的。<br />
else if (EMPTY_BIN_P(bin))<br />
return UNDEFINED_ENTRY_IND;<br />
#ifdef QUADRATIC_PROBE<br />
//启用了二次探测,计算下一个探测位置。<br />
ind = hash_bin(ind + d, tab);<br />
d++;<br />
#else<br />
//或者二次哈希<br />
ind = secondary_hash(ind, tab, &peterb);<br />
#endif<br />
COLLISION;<br />
}<br />
return bin;<br />
</pre><br />
<br />
默认采用二次哈希。<br />
<br />
* 查找过程简单明了:<br />
<br />
<pre><br />
/* Find an entry with KEY in table TAB. Return non-zero if we found<br />
it. Set up *RECORD to the found entry record. */<br />
int<br />
st_lookup(st_table *tab, st_data_t key, st_data_t *value)<br />
{<br />
st_index_t bin;<br />
//计算哈希<br />
st_hash_t hash = do_hash(key, tab);<br />
<br />
if (tab->bins == NULL) {<br />
//线性查找,对于少于等于 16 个元素的table<br />
bin = find_entry(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
} else {<br />
//进入哈希查找。<br />
bin = find_table_entry_ind(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
bin -= ENTRY_BASE;<br />
}<br />
//赋值,返回<br />
if (value != 0)<br />
*value = tab->entries[bin].record;<br />
return 1;<br />
}<br />
<br />
</pre><br />
<br />
* 删除是标记删除,开放地址法必须这么做,否则冲突的时候,前面的删除,就找不到后面的元素了:<br />
<pre><br />
bin = get_bin(tab->bins, get_size_ind(tab), bin_ind) - ENTRY_BASE;<br />
MARK_BIN_DELETED(tab, bin_ind);<br />
}<br />
entry = &tab->entries[bin];<br />
*key = entry->key;<br />
if (value != 0) *value = entry->record;<br />
MARK_ENTRY_DELETED(entry);<br />
</pre><br />
<br />
== 闭包 ==<br />
* rb_block_struct 和 rb_control_frame_struct 两个结构,闭包本质是代码块和它的上下文环境(EP指针)。rb_block_t 结构体是 rb_control_frame_t 的一部分,避免分配。但是在最新代码里已经修改了, rb_struct 变成一个 union,其中 rb_captured_block 是 rb_control_frame_struct 一部分 :<br />
<br />
<pre><br />
typedef struct rb_control_frame_struct {<br />
const VALUE *pc; /* cfp[0] */<br />
VALUE *sp; /* cfp[1] */<br />
const rb_iseq_t *iseq; /* cfp[2] */<br />
VALUE self; /* cfp[3] / block[0] */<br />
const VALUE *ep; /* cfp[4] / block[1] */<br />
const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */<br />
<br />
#if VM_DEBUG_BP_CHECK<br />
VALUE *bp_check; /* cfp[6] */<br />
#endif<br />
} rb_control_frame_t;<br />
<br />
<br />
enum rb_block_type {<br />
block_type_iseq,<br />
block_type_ifunc,<br />
block_type_symbol,<br />
block_type_proc<br />
};<br />
<br />
struct rb_block {<br />
union {<br />
struct rb_captured_block captured;<br />
VALUE symbol;<br />
VALUE proc;<br />
} as;<br />
enum rb_block_type type;<br />
};<br />
<br />
typedef struct rb_control_frame_struct {<br />
const VALUE *pc; /* cfp[0] */<br />
VALUE *sp; /* cfp[1] */<br />
const rb_iseq_t *iseq; /* cfp[2] */<br />
VALUE self; /* cfp[3] / block[0] */<br />
const VALUE *ep; /* cfp[4] / block[1] */<br />
const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */<br />
<br />
#if VM_DEBUG_BP_CHECK<br />
VALUE *bp_check; /* cfp[6] */<br />
#endif<br />
} rb_control_frame_t;<br />
<br />
</pre><br />
<br />
== 垃圾回收 ==<br />
<br />
* MRI 使用的标记清除算法,在标记和清除的时候会暂停程序,在 1.9.3 开始引入了延迟清除(lazy sweep)的优化,降低垃圾回收每次带来的暂停时间,但是并不会减少整个垃圾回收的工作量。<br />
* 可以通过 GC.start 来强制发起 full gc<br />
* GC::Profiler.enable 和 GC::Profiler.report 提供了 GC 报告,总体上来说 gc 消耗的时间跟堆的大小成线性关系。<br />
* JRuby 的 gc 就是 JVM 的 gc,比如复制收集、分代收集、并发收集等等,不再重复。</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-04-02T05:39:13Z
<p>Dennis zhuang:/* 垃圾回收 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。参考 http://ju.outofmemory.cn/entry/135587<br />
<br />
* Ruby 的方法缓存包含两层:全局的方法缓存,用于缓存接收者和实现类之间的映射,因为方法查找是要遍历整个继承链的,缓存可以加速这个调用。其次是内联方法缓存,缓存 Ruby 已经执行的已编译的 YARV 指令信息,这样可以避免查找,加速的原理和 clojure 的 direct linking 技术是一样的。无论是定义新方法、include 模块或者其他类似的操作, Ruby 都会去清空这两个缓冲。<br />
* 多次include 不同模块,最近 include 的模块作为直接超类向上延伸。<br />
* 模块也可以 include 模块,规则与类 include 模块一致,也是副本插入作为超类,作为目标类和原始超类之间新的超类。<br />
* Module prepend 例子:<br />
<br />
<pre><br />
module Professor<br />
def name<br />
"Prof. #{super}"<br />
end<br />
end<br />
class Mathematician<br />
attr_accessor :name<br />
prepend Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.name = 'Johann Carl Friedrich Gauss'<br />
<br />
p p.name<br />
<br />
</pre><br />
<br />
* prepend 虽然仍然会将 Professor 设置为 Mathematician 的新超类,但是同时会拷贝一份 Mathematician 作为 Mathematician 原生类(Origin class),将这个原生类作为 Professor 的超类,这就可以解释为什么 Professor#name 的 super 能调用到 Mathematician 的 name 方法。参考 http://ju.outofmemory.cn/entry/135588<br />
* 修改已被 include 模块,比如增加方法,所有 include 该模块的类都将包含新方法,因为共享 m_tbl 方法表,Ruby 在 include 的时候拷贝的只是 RClass struct,不拷贝底层的方法表 ,看下面例子:<br />
<br />
<pre><br />
module Professor<br />
def letcures ; end<br />
end<br />
<br />
class Mathematician<br />
attr_accessor :first_name<br />
attr_accessor :last_name<br />
<br />
include Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.first_name = 'hello'<br />
p.last_name = 'world'<br />
<br />
p p.methods.sort<br />
<br />
#open Professor, adds new method<br />
module Professor<br />
def classroom; end<br />
end<br />
<br />
p p.methods.sort<br />
<br />
</pre><br />
<br />
* 但是修改已被 include 模块中 include 的其他模块,不会影响插入到 include 类的已经被拷贝的模块副本,也就不会增加或者删除方法。<br />
* 当创建一个 class或者模块的时候 ,其实是新建了一层词法作用域,Ruby 用两个指针来标示: nd_clss ,指向当前作用域对应的模块或者类;nd_next 指向父层或者上下文的词法作用域。形成一个作用域链条。<br />
* 常量的查找跟方法的查找类似,只是方法的查找是通过祖先连(super) 来查找,而常量是通过迭代词法作用域链(nd_next)来查找。<br />
* Ruby 优先通过词法作用域来查找常量:<br />
<br />
<pre><br />
class SuperClass<br />
FIND_ME = "Found in Superclass"<br />
end<br />
<br />
module ParentLexicalScope<br />
FIND_ME = "Found in ParentLexicalScope"<br />
<br />
module ChildLexicalScope<br />
<br />
class SubClass < SuperClass<br />
p FIND_ME<br />
end<br />
end<br />
end<br />
<br />
</pre><br />
<br />
输出 "Found in ParentLexicalScope"<br />
<br />
* 真实的 Ruby 常量查找还需要加入 autoload 关键字:<br />
<br />
『检索词法作用域链 -> 为了个作用域的类检查 autoload -> 检索超类链 -> 为每个超类检查 autoload -> 调用 const_missing。』。<br />
<br />
* 关于 autoload http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html<br />
<br />
== 散列表 ==<br />
<br />
* ruby 的散列表 hash 解决哈希冲突还是经典的链表法,密度阈值设定为 5,超过就做 rehash。也就是 java hash 的所谓负载因子。<br />
* rehash 扩容的大小不是翻倍之类的算法,而是基于素数,总是将容器数目设置为一个素数表里的素数大小。这个考虑也是基于对哈希函数不够分布的担忧。<br />
* 比较元素通过 eq? 方法。<br />
* 默认哈希算法采用 murmur hash,这跟 clojure 是一样的。自定义对象作为 key,同样也可以选择自定义实现 hash 函数。一般都推荐使用默认。<br />
* Ruby 2.0 做了个优化,跟 clojure 一样,小于等于 6 个元素的 hash 直接组织成一个数组, clojure 里是少于等于8个就是 PersistentArrayMap,节省内存和提升效率。<br />
* RHash 转移到 internal.h<br />
<br />
<pre><br />
struct RHash {<br />
struct RBasic basic;<br />
struct st_table *ntbl; /* possibly 0 */<br />
int iter_lev;<br />
const VALUE ifnone;<br />
};<br />
</pre><br />
<br />
* ruby 2.4 又有一个大的变化,使用开放地址法替换了链表法,参考 https://bugs.ruby-lang.org/issues/12142,这个讨论非常有价值。<br />
<br />
<pre><br />
/* The original package implemented classic bucket-based hash tables<br />
with entries doubly linked for an access by their insertion order.<br />
To decrease pointer chasing and as a consequence to improve a data<br />
locality the current implementation is based on storing entries in<br />
an array and using hash tables with open addressing. The current<br />
entries are more compact in comparison with the original ones and<br />
this also improves the data locality.<br />
<br />
The hash table has two arrays called *bins* and *entries*.<br />
<br />
bins:<br />
-------<br />
| | entries array:<br />
|-------| --------------------------------<br />
| index | | | entry: | | |<br />
|-------| | | | | |<br />
| ... | | ... | hash | ... | ... |<br />
|-------| | | key | | |<br />
| empty | | | record | | |<br />
|-------| --------------------------------<br />
| ... | ^ ^<br />
|-------| |_ entries start |_ entries bound<br />
|deleted|<br />
-------<br />
<br />
o The entry array contains table entries in the same order as they<br />
were inserted.<br />
<br />
When the first entry is deleted, a variable containing index of<br />
the current first entry (*entries start*) is changed. In all<br />
other cases of the deletion, we just mark the entry as deleted by<br />
using a reserved hash value.<br />
<br />
Such organization of the entry storage makes operations of the<br />
table shift and the entries traversal very fast.<br />
<br />
o The bins provide access to the entries by their keys. The<br />
key hash is mapped to a bin containing *index* of the<br />
corresponding entry in the entry array.<br />
<br />
The bin array size is always power of two, it makes mapping very<br />
fast by using the corresponding lower bits of the hash.<br />
Generally it is not a good idea to ignore some part of the hash.<br />
But alternative approach is worse. For example, we could use a<br />
modulo operation for mapping and a prime number for the size of<br />
the bin array. Unfortunately, the modulo operation for big<br />
64-bit numbers are extremely slow (it takes more than 100 cycles<br />
on modern Intel CPUs).<br />
<br />
Still other bits of the hash value are used when the mapping<br />
results in a collision. In this case we use a secondary hash<br />
value which is a result of a function of the collision bin<br />
index and the original hash value. The function choice<br />
guarantees that we can traverse all bins and finally find the<br />
corresponding bin as after several iterations the function<br />
becomes a full cycle linear congruential generator because it<br />
satisfies requirements of the Hull-Dobell theorem.<br />
<br />
When an entry is removed from the table besides marking the<br />
hash in the corresponding entry described above, we also mark<br />
the bin by a special value in order to find entries which had<br />
a collision with the removed entries.<br />
<br />
There are two reserved values for the bins. One denotes an<br />
empty bin, another one denotes a bin for a deleted entry.<br />
<br />
o The length of the bin array is at least two times more than the<br />
entry array length. This keeps the table load factor healthy.<br />
The trigger of rebuilding the table is always a case when we can<br />
not insert an entry anymore at the entries bound. We could<br />
change the entries bound too in case of deletion but than we need<br />
a special code to count bins with corresponding deleted entries<br />
and reset the bin values when there are too many bins<br />
corresponding deleted entries<br />
<br />
Table rebuilding is done by creation of a new entry array and<br />
bins of an appropriate size. We also try to reuse the arrays<br />
in some cases by compacting the array and removing deleted<br />
entries.<br />
<br />
o To save memory very small tables have no allocated arrays<br />
bins. We use a linear search for an access by a key.<br />
<br />
o To save more memory we use 8-, 16-, 32- and 64- bit indexes in<br />
bins depending on the current hash table size.<br />
<br />
This implementation speeds up the Ruby hash table benchmarks in<br />
average by more 40% on Intel Haswell CPU.<br />
<br />
*/<br />
<br />
</pre><br />
<br />
=== 2.4 st_table 阅读心得 ===<br />
* st_features 定义了 table 的属性:<br />
** entry_power entries数组的大小,2的幂指数。<br />
** bin_power bins数组大小,同样是2的幂指数<br />
** size_ind 根据 table 大小,选择 bins 对应的元素的大小,可能是 8-bit, 16-bit etc。<br />
** bins_words bins按照word计算的大小。<br />
** 根据 SIZEOF_ST_INDEX_T 枚举了一堆 table 属性。<br />
* 因为 bins 的大小都是 2 的次幂,因此计算哈希值对应的 bin 可以直接用位运算:<br />
<pre><br />
/* Return mask for a bin index in table TAB. */<br />
static inline st_index_t<br />
bins_mask(const st_table *tab)<br />
{<br />
return get_bins_num(tab) - 1;<br />
}<br />
<br />
/* Return the index of table TAB bin corresponding to<br />
HASH_VALUE. */<br />
static inline st_index_t<br />
hash_bin(st_hash_t hash_value, st_table *tab)<br />
{<br />
return hash_value & bins_mask(tab);<br />
}<br />
</pre><br />
* 最小的 table 大小是 4, 由 MINIMAL_POWER2 决定。最大的 table 大小是 2 的 30 次方(非 8 位平台),8 位平台上是 2 的 62 次方。<br />
* 对于小于等于 16 个元素的 table,不创建 bins 数组,直接存储在 entries 数组,线性探测,无需进行 hash 计算和查找。<br />
* rebuild_table 可能有两种: compact 现有的,或者创建新的。当 entries_bound 到达上限的时候,开始 rebuild。<br />
** 当已有 entries 数组的大小在现有元素大小的 2 倍到 4 倍(REBUILD_THRESHOLD)之间,或者元素数量小于 16 个,进入压缩流程,直接使用原来 table 作为新 new_tab<br />
** 否则,进入新建 table 作为 new_tab<br />
** 小技巧 , prefetch 指令,预加载下个元素,在遍历 entries 的时候用到。<br />
<pre><br />
#define PREFETCH(addr, write_p) __builtin_prefetch(addr, write_p)<br />
PREFETCH(entries + i + 1, 0);<br />
</pre><br />
** rehash 其实很简单了,遍历已有的 entries 数组,跳过已经删除的,设置到新的 table 里,同时设置 bins:<br />
<pre><br />
bins = new_tab->bins;<br />
size_ind = get_size_ind(new_tab);<br />
for (i = tab->entries_start; i < bound; i++) {<br />
curr_entry_ptr = &entries[i];<br />
PREFETCH(entries + i + 1, 0);<br />
if (EXPECT(DELETED_ENTRY_P(curr_entry_ptr), 0))<br />
continue;<br />
if (&new_entries[ni] != curr_entry_ptr)<br />
new_entries[ni] = *curr_entry_ptr;<br />
if (EXPECT(bins != NULL, 1)) {<br />
bin_ind = find_table_bin_ind_direct(new_tab, curr_entry_ptr->hash,<br />
curr_entry_ptr->key);<br />
st_assert(bin_ind != UNDEFINED_BIN_IND<br />
&& (tab == new_tab || new_tab->rebuilds_num == 0)<br />
&& IND_EMPTY_BIN_P(new_tab, bin_ind));<br />
set_bin(bins, size_ind, bin_ind, ni + ENTRY_BASE);<br />
}<br />
new_tab->num_entries++;<br />
ni++;<br />
}<br />
<br />
</pre><br />
<br />
* 开放地址法,遇到哈希冲突,采用二次哈希,次级哈希函数如下:<br />
<pre><br />
/* Return the next secondary hash index for table TAB using previous<br />
index IND and PERTERB. Finally modulo of the function becomes a<br />
full *cycle linear congruential generator*, in other words it<br />
guarantees traversing all table bins in extreme case.<br />
<br />
According the Hull-Dobell theorem a generator<br />
"Xnext = (a*Xprev + c) mod m" is a full cycle generator iff<br />
o m and c are relatively prime<br />
o a-1 is divisible by all prime factors of m<br />
o a-1 is divisible by 4 if m is divisible by 4.<br />
<br />
For our case a is 5, c is 1, and m is a power of two. */<br />
static inline st_index_t<br />
secondary_hash(st_index_t ind, st_table *tab, st_index_t *perterb)<br />
{<br />
*perterb >>= 11;<br />
ind = (ind << 2) + ind + *perterb + 1;<br />
return hash_bin(ind, tab);<br />
}<br />
<br />
</pre><br />
<br />
* find_entry 线性探测,find_table_entry_ind 是二次哈希查找,但是可以选择是否启用二次探测:<br />
<pre><br />
/* Use the quadratic probing. The method has a better data locality<br />
but more collisions than the current approach. In average it<br />
results in a bit slower search. */<br />
/*#define QUADRATIC_PROBE*/<br />
<br />
ind = hash_bin(hash_value, tab);<br />
#ifdef QUADRATIC_PROBE<br />
d = 1;<br />
#else<br />
peterb = hash_value;<br />
#endif<br />
FOUND_BIN;<br />
for (;;) {<br />
bin = get_bin(tab->bins, get_size_ind(tab), ind);<br />
//找到相等的。<br />
if (! EMPTY_OR_DELETED_BIN_P(bin)<br />
&& PTR_EQUAL(tab, &entries[bin - ENTRY_BASE], hash_value, key))<br />
break;<br />
//或者找到空的。<br />
else if (EMPTY_BIN_P(bin))<br />
return UNDEFINED_ENTRY_IND;<br />
#ifdef QUADRATIC_PROBE<br />
//启用了二次探测,计算下一个探测位置。<br />
ind = hash_bin(ind + d, tab);<br />
d++;<br />
#else<br />
//或者二次哈希<br />
ind = secondary_hash(ind, tab, &peterb);<br />
#endif<br />
COLLISION;<br />
}<br />
return bin;<br />
</pre><br />
<br />
默认采用二次哈希。<br />
<br />
* 查找过程简单明了:<br />
<br />
<pre><br />
/* Find an entry with KEY in table TAB. Return non-zero if we found<br />
it. Set up *RECORD to the found entry record. */<br />
int<br />
st_lookup(st_table *tab, st_data_t key, st_data_t *value)<br />
{<br />
st_index_t bin;<br />
//计算哈希<br />
st_hash_t hash = do_hash(key, tab);<br />
<br />
if (tab->bins == NULL) {<br />
//线性查找,对于少于等于 16 个元素的table<br />
bin = find_entry(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
} else {<br />
//进入哈希查找。<br />
bin = find_table_entry_ind(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
bin -= ENTRY_BASE;<br />
}<br />
//赋值,返回<br />
if (value != 0)<br />
*value = tab->entries[bin].record;<br />
return 1;<br />
}<br />
<br />
</pre><br />
<br />
* 删除是标记删除,开放地址法必须这么做,否则冲突的时候,前面的删除,就找不到后面的元素了:<br />
<pre><br />
bin = get_bin(tab->bins, get_size_ind(tab), bin_ind) - ENTRY_BASE;<br />
MARK_BIN_DELETED(tab, bin_ind);<br />
}<br />
entry = &tab->entries[bin];<br />
*key = entry->key;<br />
if (value != 0) *value = entry->record;<br />
MARK_ENTRY_DELETED(entry);<br />
</pre><br />
<br />
== 垃圾回收 ==<br />
<br />
* MRI 使用的标记清除算法,在标记和清除的时候会暂停程序,在 1.9.3 开始引入了延迟清除(lazy sweep)的优化,降低垃圾回收每次带来的暂停时间,但是并不会减少整个垃圾回收的工作量。<br />
* 可以通过 GC.start 来强制发起 full gc<br />
* GC::Profiler.enable 和 GC::Profiler.report 提供了 GC 报告,总体上来说 gc 消耗的时间跟堆的大小成线性关系。<br />
* JRuby 的 gc 就是 JVM 的 gc,比如复制收集、分代收集、并发收集等等,不再重复。</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=%E8%AE%BA%E6%96%87%E9%98%85%E8%AF%BB%E8%AE%A1%E5%88%92
论文阅读计划
2017-02-15T15:22:13Z
<p>Dennis zhuang:/* 待读论文 */</p>
<hr />
<div>= Language & Platform =<br />
<br />
== 待读论文 ==<br />
<br />
* Making reliable distributed systems in the presence of software errors<br />
<br />
== 已读论文 ==<br />
<br />
* Characterizing the Scalability of Erlang VM on Many core Processors<br />
* The Development of the C Language<br />
<br />
= 一致性算法和分布式存储 =<br />
<br />
== 待读论文 ==<br />
<br />
* Consensus on Transaction Commit<br />
* CONSENSUS: BRIDGING THEORY AND PRACTICE<br />
* Viewstamped Replication Revisited<br />
* Zookeeper: wait-free coordination for internet-scale systems<br />
* Zab: High-performance broadcast for primary-backup systems<br />
<br />
== 已读论文 ==<br />
<br />
* [http://research.microsoft.com/en-us/um/people/lamport/pubs/paxos-simple.pdf Paxos Made Simple]<br />
* [http://research.google.com/archive/paxos_made_live.html Paxos Made Live]<br />
* [https://docs.google.com/viewer?url=https%3A%2F%2Fraft.github.io%2Fraft.pdf In Search of an Understandable Consensus Algorithm]<br />
* F1: A Distributed SQL Database That Scales<br />
* Spanner: Google’s Globally-Distributed Database<br />
* Paxos Made Practical<br />
* Dynamo: Amazon’s Highly Available Key-value Store<br />
* Building Consistent Transactions with Inconsistent Replication<br />
* The Chubby lock service for loosely-coupled distributed systems<br />
* Megastore: Providing Scalable, Highly Available Storage for Interactive Services<br />
<br />
= 负载均衡 =<br />
<br />
== 已读论文 ==<br />
<br />
* [https://www.microsoft.com/en-us/research/publication/join-idle-queue-a-novel-load-balancing-algorithm-for-dynamically-scalable-web-services/ Join-Idle-Queue: A Novel Load Balancing Algorithm for Dynamically Scalable Web Services]<br />
<br />
= 并发 =<br />
<br />
== 已读论文 ==<br />
<br />
* [https://docs.google.com/viewer?url=http%3A%2F%2Fweb.cecs.pdx.edu%2F~walpole%2Fclass%2Fcs533%2Fwinter2011%2Fslides%2F8b.pdf spin lock performance]<br />
<br />
= 算法和数据结构 =<br />
<br />
== 已读论文 ==<br />
<br />
* [http://www.cs.rochester.edu/~scott/papers/1996_PODC_queues.pdf Simple, Fast and Practical Non-Blocking and Blocking Concurrent Queue Algorithms]<br />
<br />
== 待读论文 ==<br />
<br />
* Compiling Pattern Matching to good Decision Trees<br />
* Extending Finite Automata to Efficiently Match Perl-Compatible Regular Expressions<br />
* The Bw-Tree: A B-tree for New Hardware Platforms</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=%E8%AE%BA%E6%96%87%E9%98%85%E8%AF%BB%E8%AE%A1%E5%88%92
论文阅读计划
2017-02-15T15:21:30Z
<p>Dennis zhuang:</p>
<hr />
<div>= Language & Platform =<br />
<br />
== 待读论文 ==<br />
<br />
* Making reliable distributed systems in the presence of sodware errors<br />
<br />
== 已读论文 ==<br />
<br />
* Characterizing the Scalability of Erlang VM on Many core Processors<br />
* The Development of the C Language<br />
<br />
= 一致性算法和分布式存储 =<br />
<br />
== 待读论文 ==<br />
<br />
* Consensus on Transaction Commit<br />
* CONSENSUS: BRIDGING THEORY AND PRACTICE<br />
* Viewstamped Replication Revisited<br />
* Zookeeper: wait-free coordination for internet-scale systems<br />
* Zab: High-performance broadcast for primary-backup systems<br />
<br />
== 已读论文 ==<br />
<br />
* [http://research.microsoft.com/en-us/um/people/lamport/pubs/paxos-simple.pdf Paxos Made Simple]<br />
* [http://research.google.com/archive/paxos_made_live.html Paxos Made Live]<br />
* [https://docs.google.com/viewer?url=https%3A%2F%2Fraft.github.io%2Fraft.pdf In Search of an Understandable Consensus Algorithm]<br />
* F1: A Distributed SQL Database That Scales<br />
* Spanner: Google’s Globally-Distributed Database<br />
* Paxos Made Practical<br />
* Dynamo: Amazon’s Highly Available Key-value Store<br />
* Building Consistent Transactions with Inconsistent Replication<br />
* The Chubby lock service for loosely-coupled distributed systems<br />
* Megastore: Providing Scalable, Highly Available Storage for Interactive Services<br />
<br />
= 负载均衡 =<br />
<br />
== 已读论文 ==<br />
<br />
* [https://www.microsoft.com/en-us/research/publication/join-idle-queue-a-novel-load-balancing-algorithm-for-dynamically-scalable-web-services/ Join-Idle-Queue: A Novel Load Balancing Algorithm for Dynamically Scalable Web Services]<br />
<br />
= 并发 =<br />
<br />
== 已读论文 ==<br />
<br />
* [https://docs.google.com/viewer?url=http%3A%2F%2Fweb.cecs.pdx.edu%2F~walpole%2Fclass%2Fcs533%2Fwinter2011%2Fslides%2F8b.pdf spin lock performance]<br />
<br />
= 算法和数据结构 =<br />
<br />
== 已读论文 ==<br />
<br />
* [http://www.cs.rochester.edu/~scott/papers/1996_PODC_queues.pdf Simple, Fast and Practical Non-Blocking and Blocking Concurrent Queue Algorithms]<br />
<br />
== 待读论文 ==<br />
<br />
* Compiling Pattern Matching to good Decision Trees<br />
* Extending Finite Automata to Efficiently Match Perl-Compatible Regular Expressions<br />
* The Bw-Tree: A B-tree for New Hardware Platforms</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=%E8%AE%BA%E6%96%87%E9%98%85%E8%AF%BB%E8%AE%A1%E5%88%92
论文阅读计划
2017-01-20T02:40:23Z
<p>Dennis zhuang:</p>
<hr />
<div>= Erlang =<br />
<br />
== 待读论文 ==<br />
<br />
* Making reliable distributed systems in the presence of sodware errors<br />
<br />
== 已读论文 ==<br />
<br />
* Characterizing the Scalability of Erlang VM on Many core Processors<br />
<br />
= 一致性算法和分布式存储 =<br />
<br />
== 待读论文 ==<br />
<br />
* Consensus on Transaction Commit<br />
* CONSENSUS: BRIDGING THEORY AND PRACTICE<br />
* Viewstamped Replication Revisited<br />
* Zookeeper: wait-free coordination for internet-scale systems<br />
* Zab: High-performance broadcast for primary-backup systems<br />
<br />
== 已读论文 ==<br />
<br />
* [http://research.microsoft.com/en-us/um/people/lamport/pubs/paxos-simple.pdf Paxos Made Simple]<br />
* [http://research.google.com/archive/paxos_made_live.html Paxos Made Live]<br />
* [https://docs.google.com/viewer?url=https%3A%2F%2Fraft.github.io%2Fraft.pdf In Search of an Understandable Consensus Algorithm]<br />
* F1: A Distributed SQL Database That Scales<br />
* Spanner: Google’s Globally-Distributed Database<br />
* Paxos Made Practical<br />
* Dynamo: Amazon’s Highly Available Key-value Store<br />
* Building Consistent Transactions with Inconsistent Replication<br />
* The Chubby lock service for loosely-coupled distributed systems<br />
* Megastore: Providing Scalable, Highly Available Storage for Interactive Services<br />
<br />
= 负载均衡 =<br />
<br />
== 已读论文 ==<br />
<br />
* [https://www.microsoft.com/en-us/research/publication/join-idle-queue-a-novel-load-balancing-algorithm-for-dynamically-scalable-web-services/ Join-Idle-Queue: A Novel Load Balancing Algorithm for Dynamically Scalable Web Services]<br />
<br />
= 并发 =<br />
<br />
== 已读论文 ==<br />
<br />
* [https://docs.google.com/viewer?url=http%3A%2F%2Fweb.cecs.pdx.edu%2F~walpole%2Fclass%2Fcs533%2Fwinter2011%2Fslides%2F8b.pdf spin lock performance]<br />
<br />
= 算法和数据结构 =<br />
<br />
== 已读论文 ==<br />
<br />
* [http://www.cs.rochester.edu/~scott/papers/1996_PODC_queues.pdf Simple, Fast and Practical Non-Blocking and Blocking Concurrent Queue Algorithms]<br />
<br />
== 待读论文 ==<br />
<br />
* Compiling Pattern Matching to good Decision Trees<br />
* Extending Finite Automata to Efficiently Match Perl-Compatible Regular Expressions<br />
* The Bw-Tree: A B-tree for New Hardware Platforms</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-01-19T15:18:53Z
<p>Dennis zhuang:/* 2.4 st_table 阅读心得 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。参考 http://ju.outofmemory.cn/entry/135587<br />
<br />
* Ruby 的方法缓存包含两层:全局的方法缓存,用于缓存接收者和实现类之间的映射,因为方法查找是要遍历整个继承链的,缓存可以加速这个调用。其次是内联方法缓存,缓存 Ruby 已经执行的已编译的 YARV 指令信息,这样可以避免查找,加速的原理和 clojure 的 direct linking 技术是一样的。无论是定义新方法、include 模块或者其他类似的操作, Ruby 都会去清空这两个缓冲。<br />
* 多次include 不同模块,最近 include 的模块作为直接超类向上延伸。<br />
* 模块也可以 include 模块,规则与类 include 模块一致,也是副本插入作为超类,作为目标类和原始超类之间新的超类。<br />
* Module prepend 例子:<br />
<br />
<pre><br />
module Professor<br />
def name<br />
"Prof. #{super}"<br />
end<br />
end<br />
class Mathematician<br />
attr_accessor :name<br />
prepend Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.name = 'Johann Carl Friedrich Gauss'<br />
<br />
p p.name<br />
<br />
</pre><br />
<br />
* prepend 虽然仍然会将 Professor 设置为 Mathematician 的新超类,但是同时会拷贝一份 Mathematician 作为 Mathematician 原生类(Origin class),将这个原生类作为 Professor 的超类,这就可以解释为什么 Professor#name 的 super 能调用到 Mathematician 的 name 方法。参考 http://ju.outofmemory.cn/entry/135588<br />
* 修改已被 include 模块,比如增加方法,所有 include 该模块的类都将包含新方法,因为共享 m_tbl 方法表,Ruby 在 include 的时候拷贝的只是 RClass struct,不拷贝底层的方法表 ,看下面例子:<br />
<br />
<pre><br />
module Professor<br />
def letcures ; end<br />
end<br />
<br />
class Mathematician<br />
attr_accessor :first_name<br />
attr_accessor :last_name<br />
<br />
include Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.first_name = 'hello'<br />
p.last_name = 'world'<br />
<br />
p p.methods.sort<br />
<br />
#open Professor, adds new method<br />
module Professor<br />
def classroom; end<br />
end<br />
<br />
p p.methods.sort<br />
<br />
</pre><br />
<br />
* 但是修改已被 include 模块中 include 的其他模块,不会影响插入到 include 类的已经被拷贝的模块副本,也就不会增加或者删除方法。<br />
* 当创建一个 class或者模块的时候 ,其实是新建了一层词法作用域,Ruby 用两个指针来标示: nd_clss ,指向当前作用域对应的模块或者类;nd_next 指向父层或者上下文的词法作用域。形成一个作用域链条。<br />
* 常量的查找跟方法的查找类似,只是方法的查找是通过祖先连(super) 来查找,而常量是通过迭代词法作用域链(nd_next)来查找。<br />
* Ruby 优先通过词法作用域来查找常量:<br />
<br />
<pre><br />
class SuperClass<br />
FIND_ME = "Found in Superclass"<br />
end<br />
<br />
module ParentLexicalScope<br />
FIND_ME = "Found in ParentLexicalScope"<br />
<br />
module ChildLexicalScope<br />
<br />
class SubClass < SuperClass<br />
p FIND_ME<br />
end<br />
end<br />
end<br />
<br />
</pre><br />
<br />
输出 "Found in ParentLexicalScope"<br />
<br />
* 真实的 Ruby 常量查找还需要加入 autoload 关键字:<br />
<br />
『检索词法作用域链 -> 为了个作用域的类检查 autoload -> 检索超类链 -> 为每个超类检查 autoload -> 调用 const_missing。』。<br />
<br />
* 关于 autoload http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html<br />
<br />
== 散列表 ==<br />
<br />
* ruby 的散列表 hash 解决哈希冲突还是经典的链表法,密度阈值设定为 5,超过就做 rehash。也就是 java hash 的所谓负载因子。<br />
* rehash 扩容的大小不是翻倍之类的算法,而是基于素数,总是将容器数目设置为一个素数表里的素数大小。这个考虑也是基于对哈希函数不够分布的担忧。<br />
* 比较元素通过 eq? 方法。<br />
* 默认哈希算法采用 murmur hash,这跟 clojure 是一样的。自定义对象作为 key,同样也可以选择自定义实现 hash 函数。一般都推荐使用默认。<br />
* Ruby 2.0 做了个优化,跟 clojure 一样,小于等于 6 个元素的 hash 直接组织成一个数组, clojure 里是少于等于8个就是 PersistentArrayMap,节省内存和提升效率。<br />
* RHash 转移到 internal.h<br />
<br />
<pre><br />
struct RHash {<br />
struct RBasic basic;<br />
struct st_table *ntbl; /* possibly 0 */<br />
int iter_lev;<br />
const VALUE ifnone;<br />
};<br />
</pre><br />
<br />
* ruby 2.4 又有一个大的变化,使用开放地址法替换了链表法,参考 https://bugs.ruby-lang.org/issues/12142,这个讨论非常有价值。<br />
<br />
<pre><br />
/* The original package implemented classic bucket-based hash tables<br />
with entries doubly linked for an access by their insertion order.<br />
To decrease pointer chasing and as a consequence to improve a data<br />
locality the current implementation is based on storing entries in<br />
an array and using hash tables with open addressing. The current<br />
entries are more compact in comparison with the original ones and<br />
this also improves the data locality.<br />
<br />
The hash table has two arrays called *bins* and *entries*.<br />
<br />
bins:<br />
-------<br />
| | entries array:<br />
|-------| --------------------------------<br />
| index | | | entry: | | |<br />
|-------| | | | | |<br />
| ... | | ... | hash | ... | ... |<br />
|-------| | | key | | |<br />
| empty | | | record | | |<br />
|-------| --------------------------------<br />
| ... | ^ ^<br />
|-------| |_ entries start |_ entries bound<br />
|deleted|<br />
-------<br />
<br />
o The entry array contains table entries in the same order as they<br />
were inserted.<br />
<br />
When the first entry is deleted, a variable containing index of<br />
the current first entry (*entries start*) is changed. In all<br />
other cases of the deletion, we just mark the entry as deleted by<br />
using a reserved hash value.<br />
<br />
Such organization of the entry storage makes operations of the<br />
table shift and the entries traversal very fast.<br />
<br />
o The bins provide access to the entries by their keys. The<br />
key hash is mapped to a bin containing *index* of the<br />
corresponding entry in the entry array.<br />
<br />
The bin array size is always power of two, it makes mapping very<br />
fast by using the corresponding lower bits of the hash.<br />
Generally it is not a good idea to ignore some part of the hash.<br />
But alternative approach is worse. For example, we could use a<br />
modulo operation for mapping and a prime number for the size of<br />
the bin array. Unfortunately, the modulo operation for big<br />
64-bit numbers are extremely slow (it takes more than 100 cycles<br />
on modern Intel CPUs).<br />
<br />
Still other bits of the hash value are used when the mapping<br />
results in a collision. In this case we use a secondary hash<br />
value which is a result of a function of the collision bin<br />
index and the original hash value. The function choice<br />
guarantees that we can traverse all bins and finally find the<br />
corresponding bin as after several iterations the function<br />
becomes a full cycle linear congruential generator because it<br />
satisfies requirements of the Hull-Dobell theorem.<br />
<br />
When an entry is removed from the table besides marking the<br />
hash in the corresponding entry described above, we also mark<br />
the bin by a special value in order to find entries which had<br />
a collision with the removed entries.<br />
<br />
There are two reserved values for the bins. One denotes an<br />
empty bin, another one denotes a bin for a deleted entry.<br />
<br />
o The length of the bin array is at least two times more than the<br />
entry array length. This keeps the table load factor healthy.<br />
The trigger of rebuilding the table is always a case when we can<br />
not insert an entry anymore at the entries bound. We could<br />
change the entries bound too in case of deletion but than we need<br />
a special code to count bins with corresponding deleted entries<br />
and reset the bin values when there are too many bins<br />
corresponding deleted entries<br />
<br />
Table rebuilding is done by creation of a new entry array and<br />
bins of an appropriate size. We also try to reuse the arrays<br />
in some cases by compacting the array and removing deleted<br />
entries.<br />
<br />
o To save memory very small tables have no allocated arrays<br />
bins. We use a linear search for an access by a key.<br />
<br />
o To save more memory we use 8-, 16-, 32- and 64- bit indexes in<br />
bins depending on the current hash table size.<br />
<br />
This implementation speeds up the Ruby hash table benchmarks in<br />
average by more 40% on Intel Haswell CPU.<br />
<br />
*/<br />
<br />
</pre><br />
<br />
=== 2.4 st_table 阅读心得 ===<br />
* st_features 定义了 table 的属性:<br />
** entry_power entries数组的大小,2的幂指数。<br />
** bin_power bins数组大小,同样是2的幂指数<br />
** size_ind 根据 table 大小,选择 bins 对应的元素的大小,可能是 8-bit, 16-bit etc。<br />
** bins_words bins按照word计算的大小。<br />
** 根据 SIZEOF_ST_INDEX_T 枚举了一堆 table 属性。<br />
* 因为 bins 的大小都是 2 的次幂,因此计算哈希值对应的 bin 可以直接用位运算:<br />
<pre><br />
/* Return mask for a bin index in table TAB. */<br />
static inline st_index_t<br />
bins_mask(const st_table *tab)<br />
{<br />
return get_bins_num(tab) - 1;<br />
}<br />
<br />
/* Return the index of table TAB bin corresponding to<br />
HASH_VALUE. */<br />
static inline st_index_t<br />
hash_bin(st_hash_t hash_value, st_table *tab)<br />
{<br />
return hash_value & bins_mask(tab);<br />
}<br />
</pre><br />
* 最小的 table 大小是 4, 由 MINIMAL_POWER2 决定。最大的 table 大小是 2 的 30 次方(非 8 位平台),8 位平台上是 2 的 62 次方。<br />
* 对于小于等于 16 个元素的 table,不创建 bins 数组,直接存储在 entries 数组,线性探测,无需进行 hash 计算和查找。<br />
* rebuild_table 可能有两种: compact 现有的,或者创建新的。当 entries_bound 到达上限的时候,开始 rebuild。<br />
** 当已有 entries 数组的大小在现有元素大小的 2 倍到 4 倍(REBUILD_THRESHOLD)之间,或者元素数量小于 16 个,进入压缩流程,直接使用原来 table 作为新 new_tab<br />
** 否则,进入新建 table 作为 new_tab<br />
** 小技巧 , prefetch 指令,预加载下个元素,在遍历 entries 的时候用到。<br />
<pre><br />
#define PREFETCH(addr, write_p) __builtin_prefetch(addr, write_p)<br />
PREFETCH(entries + i + 1, 0);<br />
</pre><br />
** rehash 其实很简单了,遍历已有的 entries 数组,跳过已经删除的,设置到新的 table 里,同时设置 bins:<br />
<pre><br />
bins = new_tab->bins;<br />
size_ind = get_size_ind(new_tab);<br />
for (i = tab->entries_start; i < bound; i++) {<br />
curr_entry_ptr = &entries[i];<br />
PREFETCH(entries + i + 1, 0);<br />
if (EXPECT(DELETED_ENTRY_P(curr_entry_ptr), 0))<br />
continue;<br />
if (&new_entries[ni] != curr_entry_ptr)<br />
new_entries[ni] = *curr_entry_ptr;<br />
if (EXPECT(bins != NULL, 1)) {<br />
bin_ind = find_table_bin_ind_direct(new_tab, curr_entry_ptr->hash,<br />
curr_entry_ptr->key);<br />
st_assert(bin_ind != UNDEFINED_BIN_IND<br />
&& (tab == new_tab || new_tab->rebuilds_num == 0)<br />
&& IND_EMPTY_BIN_P(new_tab, bin_ind));<br />
set_bin(bins, size_ind, bin_ind, ni + ENTRY_BASE);<br />
}<br />
new_tab->num_entries++;<br />
ni++;<br />
}<br />
<br />
</pre><br />
<br />
* 开放地址法,遇到哈希冲突,采用二次哈希,次级哈希函数如下:<br />
<pre><br />
/* Return the next secondary hash index for table TAB using previous<br />
index IND and PERTERB. Finally modulo of the function becomes a<br />
full *cycle linear congruential generator*, in other words it<br />
guarantees traversing all table bins in extreme case.<br />
<br />
According the Hull-Dobell theorem a generator<br />
"Xnext = (a*Xprev + c) mod m" is a full cycle generator iff<br />
o m and c are relatively prime<br />
o a-1 is divisible by all prime factors of m<br />
o a-1 is divisible by 4 if m is divisible by 4.<br />
<br />
For our case a is 5, c is 1, and m is a power of two. */<br />
static inline st_index_t<br />
secondary_hash(st_index_t ind, st_table *tab, st_index_t *perterb)<br />
{<br />
*perterb >>= 11;<br />
ind = (ind << 2) + ind + *perterb + 1;<br />
return hash_bin(ind, tab);<br />
}<br />
<br />
</pre><br />
<br />
* find_entry 线性探测,find_table_entry_ind 是二次哈希查找,但是可以选择是否启用二次探测:<br />
<pre><br />
/* Use the quadratic probing. The method has a better data locality<br />
but more collisions than the current approach. In average it<br />
results in a bit slower search. */<br />
/*#define QUADRATIC_PROBE*/<br />
<br />
ind = hash_bin(hash_value, tab);<br />
#ifdef QUADRATIC_PROBE<br />
d = 1;<br />
#else<br />
peterb = hash_value;<br />
#endif<br />
FOUND_BIN;<br />
for (;;) {<br />
bin = get_bin(tab->bins, get_size_ind(tab), ind);<br />
//找到相等的。<br />
if (! EMPTY_OR_DELETED_BIN_P(bin)<br />
&& PTR_EQUAL(tab, &entries[bin - ENTRY_BASE], hash_value, key))<br />
break;<br />
//或者找到空的。<br />
else if (EMPTY_BIN_P(bin))<br />
return UNDEFINED_ENTRY_IND;<br />
#ifdef QUADRATIC_PROBE<br />
//启用了二次探测,计算下一个探测位置。<br />
ind = hash_bin(ind + d, tab);<br />
d++;<br />
#else<br />
//或者二次哈希<br />
ind = secondary_hash(ind, tab, &peterb);<br />
#endif<br />
COLLISION;<br />
}<br />
return bin;<br />
</pre><br />
<br />
默认采用二次哈希。<br />
<br />
* 查找过程简单明了:<br />
<br />
<pre><br />
/* Find an entry with KEY in table TAB. Return non-zero if we found<br />
it. Set up *RECORD to the found entry record. */<br />
int<br />
st_lookup(st_table *tab, st_data_t key, st_data_t *value)<br />
{<br />
st_index_t bin;<br />
//计算哈希<br />
st_hash_t hash = do_hash(key, tab);<br />
<br />
if (tab->bins == NULL) {<br />
//线性查找,对于少于等于 16 个元素的table<br />
bin = find_entry(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
} else {<br />
//进入哈希查找。<br />
bin = find_table_entry_ind(tab, hash, key);<br />
if (bin == UNDEFINED_ENTRY_IND)<br />
return 0;<br />
bin -= ENTRY_BASE;<br />
}<br />
//赋值,返回<br />
if (value != 0)<br />
*value = tab->entries[bin].record;<br />
return 1;<br />
}<br />
<br />
</pre><br />
<br />
* 删除是标记删除,开放地址法必须这么做,否则冲突的时候,前面的删除,就找不到后面的元素了:<br />
<pre><br />
bin = get_bin(tab->bins, get_size_ind(tab), bin_ind) - ENTRY_BASE;<br />
MARK_BIN_DELETED(tab, bin_ind);<br />
}<br />
entry = &tab->entries[bin];<br />
*key = entry->key;<br />
if (value != 0) *value = entry->record;<br />
MARK_ENTRY_DELETED(entry);<br />
</pre><br />
<br />
== 垃圾回收 ==</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-01-19T15:01:20Z
<p>Dennis zhuang:/* 2.4 st_table 阅读心得 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。参考 http://ju.outofmemory.cn/entry/135587<br />
<br />
* Ruby 的方法缓存包含两层:全局的方法缓存,用于缓存接收者和实现类之间的映射,因为方法查找是要遍历整个继承链的,缓存可以加速这个调用。其次是内联方法缓存,缓存 Ruby 已经执行的已编译的 YARV 指令信息,这样可以避免查找,加速的原理和 clojure 的 direct linking 技术是一样的。无论是定义新方法、include 模块或者其他类似的操作, Ruby 都会去清空这两个缓冲。<br />
* 多次include 不同模块,最近 include 的模块作为直接超类向上延伸。<br />
* 模块也可以 include 模块,规则与类 include 模块一致,也是副本插入作为超类,作为目标类和原始超类之间新的超类。<br />
* Module prepend 例子:<br />
<br />
<pre><br />
module Professor<br />
def name<br />
"Prof. #{super}"<br />
end<br />
end<br />
class Mathematician<br />
attr_accessor :name<br />
prepend Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.name = 'Johann Carl Friedrich Gauss'<br />
<br />
p p.name<br />
<br />
</pre><br />
<br />
* prepend 虽然仍然会将 Professor 设置为 Mathematician 的新超类,但是同时会拷贝一份 Mathematician 作为 Mathematician 原生类(Origin class),将这个原生类作为 Professor 的超类,这就可以解释为什么 Professor#name 的 super 能调用到 Mathematician 的 name 方法。参考 http://ju.outofmemory.cn/entry/135588<br />
* 修改已被 include 模块,比如增加方法,所有 include 该模块的类都将包含新方法,因为共享 m_tbl 方法表,Ruby 在 include 的时候拷贝的只是 RClass struct,不拷贝底层的方法表 ,看下面例子:<br />
<br />
<pre><br />
module Professor<br />
def letcures ; end<br />
end<br />
<br />
class Mathematician<br />
attr_accessor :first_name<br />
attr_accessor :last_name<br />
<br />
include Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.first_name = 'hello'<br />
p.last_name = 'world'<br />
<br />
p p.methods.sort<br />
<br />
#open Professor, adds new method<br />
module Professor<br />
def classroom; end<br />
end<br />
<br />
p p.methods.sort<br />
<br />
</pre><br />
<br />
* 但是修改已被 include 模块中 include 的其他模块,不会影响插入到 include 类的已经被拷贝的模块副本,也就不会增加或者删除方法。<br />
* 当创建一个 class或者模块的时候 ,其实是新建了一层词法作用域,Ruby 用两个指针来标示: nd_clss ,指向当前作用域对应的模块或者类;nd_next 指向父层或者上下文的词法作用域。形成一个作用域链条。<br />
* 常量的查找跟方法的查找类似,只是方法的查找是通过祖先连(super) 来查找,而常量是通过迭代词法作用域链(nd_next)来查找。<br />
* Ruby 优先通过词法作用域来查找常量:<br />
<br />
<pre><br />
class SuperClass<br />
FIND_ME = "Found in Superclass"<br />
end<br />
<br />
module ParentLexicalScope<br />
FIND_ME = "Found in ParentLexicalScope"<br />
<br />
module ChildLexicalScope<br />
<br />
class SubClass < SuperClass<br />
p FIND_ME<br />
end<br />
end<br />
end<br />
<br />
</pre><br />
<br />
输出 "Found in ParentLexicalScope"<br />
<br />
* 真实的 Ruby 常量查找还需要加入 autoload 关键字:<br />
<br />
『检索词法作用域链 -> 为了个作用域的类检查 autoload -> 检索超类链 -> 为每个超类检查 autoload -> 调用 const_missing。』。<br />
<br />
* 关于 autoload http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html<br />
<br />
== 散列表 ==<br />
<br />
* ruby 的散列表 hash 解决哈希冲突还是经典的链表法,密度阈值设定为 5,超过就做 rehash。也就是 java hash 的所谓负载因子。<br />
* rehash 扩容的大小不是翻倍之类的算法,而是基于素数,总是将容器数目设置为一个素数表里的素数大小。这个考虑也是基于对哈希函数不够分布的担忧。<br />
* 比较元素通过 eq? 方法。<br />
* 默认哈希算法采用 murmur hash,这跟 clojure 是一样的。自定义对象作为 key,同样也可以选择自定义实现 hash 函数。一般都推荐使用默认。<br />
* Ruby 2.0 做了个优化,跟 clojure 一样,小于等于 6 个元素的 hash 直接组织成一个数组, clojure 里是少于等于8个就是 PersistentArrayMap,节省内存和提升效率。<br />
* RHash 转移到 internal.h<br />
<br />
<pre><br />
struct RHash {<br />
struct RBasic basic;<br />
struct st_table *ntbl; /* possibly 0 */<br />
int iter_lev;<br />
const VALUE ifnone;<br />
};<br />
</pre><br />
<br />
* ruby 2.4 又有一个大的变化,使用开放地址法替换了链表法,参考 https://bugs.ruby-lang.org/issues/12142,这个讨论非常有价值。<br />
<br />
<pre><br />
/* The original package implemented classic bucket-based hash tables<br />
with entries doubly linked for an access by their insertion order.<br />
To decrease pointer chasing and as a consequence to improve a data<br />
locality the current implementation is based on storing entries in<br />
an array and using hash tables with open addressing. The current<br />
entries are more compact in comparison with the original ones and<br />
this also improves the data locality.<br />
<br />
The hash table has two arrays called *bins* and *entries*.<br />
<br />
bins:<br />
-------<br />
| | entries array:<br />
|-------| --------------------------------<br />
| index | | | entry: | | |<br />
|-------| | | | | |<br />
| ... | | ... | hash | ... | ... |<br />
|-------| | | key | | |<br />
| empty | | | record | | |<br />
|-------| --------------------------------<br />
| ... | ^ ^<br />
|-------| |_ entries start |_ entries bound<br />
|deleted|<br />
-------<br />
<br />
o The entry array contains table entries in the same order as they<br />
were inserted.<br />
<br />
When the first entry is deleted, a variable containing index of<br />
the current first entry (*entries start*) is changed. In all<br />
other cases of the deletion, we just mark the entry as deleted by<br />
using a reserved hash value.<br />
<br />
Such organization of the entry storage makes operations of the<br />
table shift and the entries traversal very fast.<br />
<br />
o The bins provide access to the entries by their keys. The<br />
key hash is mapped to a bin containing *index* of the<br />
corresponding entry in the entry array.<br />
<br />
The bin array size is always power of two, it makes mapping very<br />
fast by using the corresponding lower bits of the hash.<br />
Generally it is not a good idea to ignore some part of the hash.<br />
But alternative approach is worse. For example, we could use a<br />
modulo operation for mapping and a prime number for the size of<br />
the bin array. Unfortunately, the modulo operation for big<br />
64-bit numbers are extremely slow (it takes more than 100 cycles<br />
on modern Intel CPUs).<br />
<br />
Still other bits of the hash value are used when the mapping<br />
results in a collision. In this case we use a secondary hash<br />
value which is a result of a function of the collision bin<br />
index and the original hash value. The function choice<br />
guarantees that we can traverse all bins and finally find the<br />
corresponding bin as after several iterations the function<br />
becomes a full cycle linear congruential generator because it<br />
satisfies requirements of the Hull-Dobell theorem.<br />
<br />
When an entry is removed from the table besides marking the<br />
hash in the corresponding entry described above, we also mark<br />
the bin by a special value in order to find entries which had<br />
a collision with the removed entries.<br />
<br />
There are two reserved values for the bins. One denotes an<br />
empty bin, another one denotes a bin for a deleted entry.<br />
<br />
o The length of the bin array is at least two times more than the<br />
entry array length. This keeps the table load factor healthy.<br />
The trigger of rebuilding the table is always a case when we can<br />
not insert an entry anymore at the entries bound. We could<br />
change the entries bound too in case of deletion but than we need<br />
a special code to count bins with corresponding deleted entries<br />
and reset the bin values when there are too many bins<br />
corresponding deleted entries<br />
<br />
Table rebuilding is done by creation of a new entry array and<br />
bins of an appropriate size. We also try to reuse the arrays<br />
in some cases by compacting the array and removing deleted<br />
entries.<br />
<br />
o To save memory very small tables have no allocated arrays<br />
bins. We use a linear search for an access by a key.<br />
<br />
o To save more memory we use 8-, 16-, 32- and 64- bit indexes in<br />
bins depending on the current hash table size.<br />
<br />
This implementation speeds up the Ruby hash table benchmarks in<br />
average by more 40% on Intel Haswell CPU.<br />
<br />
*/<br />
<br />
</pre><br />
<br />
=== 2.4 st_table 阅读心得 ===<br />
* st_features 定义了 table 的属性:<br />
** entry_power entries数组的大小,2的幂指数。<br />
** bin_power bins数组大小,同样是2的幂指数<br />
** size_ind 根据 table 大小,选择 bins 对应的元素的大小,可能是 8-bit, 16-bit etc。<br />
** bins_words bins按照word计算的大小。<br />
** 根据 SIZEOF_ST_INDEX_T 枚举了一堆 table 属性。<br />
* 因为 bins 的大小都是 2 的次幂,因此计算哈希值对应的 bin 可以直接用位运算:<br />
<pre><br />
/* Return mask for a bin index in table TAB. */<br />
static inline st_index_t<br />
bins_mask(const st_table *tab)<br />
{<br />
return get_bins_num(tab) - 1;<br />
}<br />
<br />
/* Return the index of table TAB bin corresponding to<br />
HASH_VALUE. */<br />
static inline st_index_t<br />
hash_bin(st_hash_t hash_value, st_table *tab)<br />
{<br />
return hash_value & bins_mask(tab);<br />
}<br />
</pre><br />
* 最小的 table 大小是 4, 由 MINIMAL_POWER2 决定。最大的 table 大小是 2 的 30 次方(非 8 位平台),8 位平台上是 2 的 62 次方。<br />
* 对于小于等于 16 个元素的 table,不创建 bins 数组,直接存储在 entries 数组,线性探测,无需进行 hash 计算和查找。<br />
* rebuild_table 可能有两种: compact 现有的,或者创建新的<br />
** 当已有 entries 数组的大小在现有元素大小的 2 倍到 4 倍(REBUILD_THRESHOLD)之间,或者元素数量小于 16 个,进入压缩流程,直接使用原来 table 作为新 new_tab<br />
** 否则,进入新建 table 作为 new_tab<br />
** 小技巧 , prefetch 指令,预加载下个元素,在遍历 entries 的时候用到。<br />
<pre><br />
#define PREFETCH(addr, write_p) __builtin_prefetch(addr, write_p)<br />
PREFETCH(entries + i + 1, 0);<br />
</pre><br />
** rehash 其实很简单了,遍历已有的 entries 数组,跳过已经删除的,设置到新的 table 里,同时设置 bins:<br />
<pre><br />
bins = new_tab->bins;<br />
size_ind = get_size_ind(new_tab);<br />
for (i = tab->entries_start; i < bound; i++) {<br />
curr_entry_ptr = &entries[i];<br />
PREFETCH(entries + i + 1, 0);<br />
if (EXPECT(DELETED_ENTRY_P(curr_entry_ptr), 0))<br />
continue;<br />
if (&new_entries[ni] != curr_entry_ptr)<br />
new_entries[ni] = *curr_entry_ptr;<br />
if (EXPECT(bins != NULL, 1)) {<br />
bin_ind = find_table_bin_ind_direct(new_tab, curr_entry_ptr->hash,<br />
curr_entry_ptr->key);<br />
st_assert(bin_ind != UNDEFINED_BIN_IND<br />
&& (tab == new_tab || new_tab->rebuilds_num == 0)<br />
&& IND_EMPTY_BIN_P(new_tab, bin_ind));<br />
set_bin(bins, size_ind, bin_ind, ni + ENTRY_BASE);<br />
}<br />
new_tab->num_entries++;<br />
ni++;<br />
}<br />
<br />
</pre><br />
<br />
* 开放地址法,遇到哈希冲突,采用二次探测,二级哈希函数如下:<br />
<pre><br />
/* Return the next secondary hash index for table TAB using previous<br />
index IND and PERTERB. Finally modulo of the function becomes a<br />
full *cycle linear congruential generator*, in other words it<br />
guarantees traversing all table bins in extreme case.<br />
<br />
According the Hull-Dobell theorem a generator<br />
"Xnext = (a*Xprev + c) mod m" is a full cycle generator iff<br />
o m and c are relatively prime<br />
o a-1 is divisible by all prime factors of m<br />
o a-1 is divisible by 4 if m is divisible by 4.<br />
<br />
For our case a is 5, c is 1, and m is a power of two. */<br />
static inline st_index_t<br />
secondary_hash(st_index_t ind, st_table *tab, st_index_t *perterb)<br />
{<br />
*perterb >>= 11;<br />
ind = (ind << 2) + ind + *perterb + 1;<br />
return hash_bin(ind, tab);<br />
}<br />
<br />
</pre><br />
<br />
* find_entry 线性探测,find_table_entry_ind 是二次探测。二次探测很简单了,好像没什么特别好说的:<br />
<pre><br />
ind = hash_bin(hash_value, tab);<br />
#ifdef QUADRATIC_PROBE<br />
d = 1;<br />
#else<br />
peterb = hash_value;<br />
#endif<br />
FOUND_BIN;<br />
for (;;) {<br />
bin = get_bin(tab->bins, get_size_ind(tab), ind);<br />
//找到相等的。<br />
if (! EMPTY_OR_DELETED_BIN_P(bin)<br />
&& PTR_EQUAL(tab, &entries[bin - ENTRY_BASE], hash_value, key))<br />
break;<br />
//或者找到空的。<br />
else if (EMPTY_BIN_P(bin))<br />
return UNDEFINED_ENTRY_IND;<br />
#ifdef QUADRATIC_PROBE<br />
//否则,计算下一个探测位置。<br />
ind = hash_bin(ind + d, tab);<br />
d++;<br />
#else<br />
ind = secondary_hash(ind, tab, &peterb);<br />
#endif<br />
COLLISION;<br />
}<br />
return bin;<br />
</pre><br />
<br />
== 垃圾回收 ==</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-01-19T14:55:38Z
<p>Dennis zhuang:/* 2.4 st_table 阅读心得 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。参考 http://ju.outofmemory.cn/entry/135587<br />
<br />
* Ruby 的方法缓存包含两层:全局的方法缓存,用于缓存接收者和实现类之间的映射,因为方法查找是要遍历整个继承链的,缓存可以加速这个调用。其次是内联方法缓存,缓存 Ruby 已经执行的已编译的 YARV 指令信息,这样可以避免查找,加速的原理和 clojure 的 direct linking 技术是一样的。无论是定义新方法、include 模块或者其他类似的操作, Ruby 都会去清空这两个缓冲。<br />
* 多次include 不同模块,最近 include 的模块作为直接超类向上延伸。<br />
* 模块也可以 include 模块,规则与类 include 模块一致,也是副本插入作为超类,作为目标类和原始超类之间新的超类。<br />
* Module prepend 例子:<br />
<br />
<pre><br />
module Professor<br />
def name<br />
"Prof. #{super}"<br />
end<br />
end<br />
class Mathematician<br />
attr_accessor :name<br />
prepend Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.name = 'Johann Carl Friedrich Gauss'<br />
<br />
p p.name<br />
<br />
</pre><br />
<br />
* prepend 虽然仍然会将 Professor 设置为 Mathematician 的新超类,但是同时会拷贝一份 Mathematician 作为 Mathematician 原生类(Origin class),将这个原生类作为 Professor 的超类,这就可以解释为什么 Professor#name 的 super 能调用到 Mathematician 的 name 方法。参考 http://ju.outofmemory.cn/entry/135588<br />
* 修改已被 include 模块,比如增加方法,所有 include 该模块的类都将包含新方法,因为共享 m_tbl 方法表,Ruby 在 include 的时候拷贝的只是 RClass struct,不拷贝底层的方法表 ,看下面例子:<br />
<br />
<pre><br />
module Professor<br />
def letcures ; end<br />
end<br />
<br />
class Mathematician<br />
attr_accessor :first_name<br />
attr_accessor :last_name<br />
<br />
include Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.first_name = 'hello'<br />
p.last_name = 'world'<br />
<br />
p p.methods.sort<br />
<br />
#open Professor, adds new method<br />
module Professor<br />
def classroom; end<br />
end<br />
<br />
p p.methods.sort<br />
<br />
</pre><br />
<br />
* 但是修改已被 include 模块中 include 的其他模块,不会影响插入到 include 类的已经被拷贝的模块副本,也就不会增加或者删除方法。<br />
* 当创建一个 class或者模块的时候 ,其实是新建了一层词法作用域,Ruby 用两个指针来标示: nd_clss ,指向当前作用域对应的模块或者类;nd_next 指向父层或者上下文的词法作用域。形成一个作用域链条。<br />
* 常量的查找跟方法的查找类似,只是方法的查找是通过祖先连(super) 来查找,而常量是通过迭代词法作用域链(nd_next)来查找。<br />
* Ruby 优先通过词法作用域来查找常量:<br />
<br />
<pre><br />
class SuperClass<br />
FIND_ME = "Found in Superclass"<br />
end<br />
<br />
module ParentLexicalScope<br />
FIND_ME = "Found in ParentLexicalScope"<br />
<br />
module ChildLexicalScope<br />
<br />
class SubClass < SuperClass<br />
p FIND_ME<br />
end<br />
end<br />
end<br />
<br />
</pre><br />
<br />
输出 "Found in ParentLexicalScope"<br />
<br />
* 真实的 Ruby 常量查找还需要加入 autoload 关键字:<br />
<br />
『检索词法作用域链 -> 为了个作用域的类检查 autoload -> 检索超类链 -> 为每个超类检查 autoload -> 调用 const_missing。』。<br />
<br />
* 关于 autoload http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html<br />
<br />
== 散列表 ==<br />
<br />
* ruby 的散列表 hash 解决哈希冲突还是经典的链表法,密度阈值设定为 5,超过就做 rehash。也就是 java hash 的所谓负载因子。<br />
* rehash 扩容的大小不是翻倍之类的算法,而是基于素数,总是将容器数目设置为一个素数表里的素数大小。这个考虑也是基于对哈希函数不够分布的担忧。<br />
* 比较元素通过 eq? 方法。<br />
* 默认哈希算法采用 murmur hash,这跟 clojure 是一样的。自定义对象作为 key,同样也可以选择自定义实现 hash 函数。一般都推荐使用默认。<br />
* Ruby 2.0 做了个优化,跟 clojure 一样,小于等于 6 个元素的 hash 直接组织成一个数组, clojure 里是少于等于8个就是 PersistentArrayMap,节省内存和提升效率。<br />
* RHash 转移到 internal.h<br />
<br />
<pre><br />
struct RHash {<br />
struct RBasic basic;<br />
struct st_table *ntbl; /* possibly 0 */<br />
int iter_lev;<br />
const VALUE ifnone;<br />
};<br />
</pre><br />
<br />
* ruby 2.4 又有一个大的变化,使用开放地址法替换了链表法,参考 https://bugs.ruby-lang.org/issues/12142,这个讨论非常有价值。<br />
<br />
<pre><br />
/* The original package implemented classic bucket-based hash tables<br />
with entries doubly linked for an access by their insertion order.<br />
To decrease pointer chasing and as a consequence to improve a data<br />
locality the current implementation is based on storing entries in<br />
an array and using hash tables with open addressing. The current<br />
entries are more compact in comparison with the original ones and<br />
this also improves the data locality.<br />
<br />
The hash table has two arrays called *bins* and *entries*.<br />
<br />
bins:<br />
-------<br />
| | entries array:<br />
|-------| --------------------------------<br />
| index | | | entry: | | |<br />
|-------| | | | | |<br />
| ... | | ... | hash | ... | ... |<br />
|-------| | | key | | |<br />
| empty | | | record | | |<br />
|-------| --------------------------------<br />
| ... | ^ ^<br />
|-------| |_ entries start |_ entries bound<br />
|deleted|<br />
-------<br />
<br />
o The entry array contains table entries in the same order as they<br />
were inserted.<br />
<br />
When the first entry is deleted, a variable containing index of<br />
the current first entry (*entries start*) is changed. In all<br />
other cases of the deletion, we just mark the entry as deleted by<br />
using a reserved hash value.<br />
<br />
Such organization of the entry storage makes operations of the<br />
table shift and the entries traversal very fast.<br />
<br />
o The bins provide access to the entries by their keys. The<br />
key hash is mapped to a bin containing *index* of the<br />
corresponding entry in the entry array.<br />
<br />
The bin array size is always power of two, it makes mapping very<br />
fast by using the corresponding lower bits of the hash.<br />
Generally it is not a good idea to ignore some part of the hash.<br />
But alternative approach is worse. For example, we could use a<br />
modulo operation for mapping and a prime number for the size of<br />
the bin array. Unfortunately, the modulo operation for big<br />
64-bit numbers are extremely slow (it takes more than 100 cycles<br />
on modern Intel CPUs).<br />
<br />
Still other bits of the hash value are used when the mapping<br />
results in a collision. In this case we use a secondary hash<br />
value which is a result of a function of the collision bin<br />
index and the original hash value. The function choice<br />
guarantees that we can traverse all bins and finally find the<br />
corresponding bin as after several iterations the function<br />
becomes a full cycle linear congruential generator because it<br />
satisfies requirements of the Hull-Dobell theorem.<br />
<br />
When an entry is removed from the table besides marking the<br />
hash in the corresponding entry described above, we also mark<br />
the bin by a special value in order to find entries which had<br />
a collision with the removed entries.<br />
<br />
There are two reserved values for the bins. One denotes an<br />
empty bin, another one denotes a bin for a deleted entry.<br />
<br />
o The length of the bin array is at least two times more than the<br />
entry array length. This keeps the table load factor healthy.<br />
The trigger of rebuilding the table is always a case when we can<br />
not insert an entry anymore at the entries bound. We could<br />
change the entries bound too in case of deletion but than we need<br />
a special code to count bins with corresponding deleted entries<br />
and reset the bin values when there are too many bins<br />
corresponding deleted entries<br />
<br />
Table rebuilding is done by creation of a new entry array and<br />
bins of an appropriate size. We also try to reuse the arrays<br />
in some cases by compacting the array and removing deleted<br />
entries.<br />
<br />
o To save memory very small tables have no allocated arrays<br />
bins. We use a linear search for an access by a key.<br />
<br />
o To save more memory we use 8-, 16-, 32- and 64- bit indexes in<br />
bins depending on the current hash table size.<br />
<br />
This implementation speeds up the Ruby hash table benchmarks in<br />
average by more 40% on Intel Haswell CPU.<br />
<br />
*/<br />
<br />
</pre><br />
<br />
=== 2.4 st_table 阅读心得 ===<br />
* st_features 定义了 table 的属性:<br />
** entry_power entries数组的大小,2的幂指数。<br />
** bin_power bins数组大小,同样是2的幂指数<br />
** size_ind 根据 table 大小,选择 bins 对应的元素的大小,可能是 8-bit, 16-bit etc。<br />
** bins_words bins按照word计算的大小。<br />
** 根据 SIZEOF_ST_INDEX_T 枚举了一堆 table 属性。<br />
* 因为 bins 的大小都是 2 的次幂,因此计算哈希值对应的 bin 可以直接用位运算:<br />
<pre><br />
/* Return mask for a bin index in table TAB. */<br />
static inline st_index_t<br />
bins_mask(const st_table *tab)<br />
{<br />
return get_bins_num(tab) - 1;<br />
}<br />
<br />
/* Return the index of table TAB bin corresponding to<br />
HASH_VALUE. */<br />
static inline st_index_t<br />
hash_bin(st_hash_t hash_value, st_table *tab)<br />
{<br />
return hash_value & bins_mask(tab);<br />
}<br />
</pre><br />
* 最小的 table 大小是 4, 由 MINIMAL_POWER2 决定。最大的 table 大小是 2 的 30 次方(非 8 位平台),8 位平台上是 2 的 62 次方。<br />
* 对于小于等于 16 个元素的 table,不创建 bins 数组,直接存储在 entries 数组,线性探测,无需进行 hash 计算和查找。<br />
* rebuild_table 可能有两种: compact 现有的,或者创建新的<br />
** 当已有 entries 数组的大小在现有元素大小的 2 倍到 4 倍(REBUILD_THRESHOLD)之间,或者元素数量小于 16 个,进入压缩流程,直接使用原来 table 作为新 new_tab<br />
** 否则,进入新建 table 作为 new_tab<br />
** 小技巧 , prefetch 指令,预加载下个元素,在遍历 entries 的时候用到。<br />
<pre><br />
#define PREFETCH(addr, write_p) __builtin_prefetch(addr, write_p)<br />
PREFETCH(entries + i + 1, 0);<br />
</pre><br />
** rehash 其实很简单了,遍历已有的 entries 数组,跳过已经删除的,设置到新的 table 里,同时设置 bins:<br />
<pre><br />
bins = new_tab->bins;<br />
size_ind = get_size_ind(new_tab);<br />
for (i = tab->entries_start; i < bound; i++) {<br />
curr_entry_ptr = &entries[i];<br />
PREFETCH(entries + i + 1, 0);<br />
if (EXPECT(DELETED_ENTRY_P(curr_entry_ptr), 0))<br />
continue;<br />
if (&new_entries[ni] != curr_entry_ptr)<br />
new_entries[ni] = *curr_entry_ptr;<br />
if (EXPECT(bins != NULL, 1)) {<br />
bin_ind = find_table_bin_ind_direct(new_tab, curr_entry_ptr->hash,<br />
curr_entry_ptr->key);<br />
st_assert(bin_ind != UNDEFINED_BIN_IND<br />
&& (tab == new_tab || new_tab->rebuilds_num == 0)<br />
&& IND_EMPTY_BIN_P(new_tab, bin_ind));<br />
set_bin(bins, size_ind, bin_ind, ni + ENTRY_BASE);<br />
}<br />
new_tab->num_entries++;<br />
ni++;<br />
}<br />
<br />
</pre><br />
<br />
== 垃圾回收 ==</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-01-19T14:44:21Z
<p>Dennis zhuang:/* 2.4 st_table 阅读心得 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。参考 http://ju.outofmemory.cn/entry/135587<br />
<br />
* Ruby 的方法缓存包含两层:全局的方法缓存,用于缓存接收者和实现类之间的映射,因为方法查找是要遍历整个继承链的,缓存可以加速这个调用。其次是内联方法缓存,缓存 Ruby 已经执行的已编译的 YARV 指令信息,这样可以避免查找,加速的原理和 clojure 的 direct linking 技术是一样的。无论是定义新方法、include 模块或者其他类似的操作, Ruby 都会去清空这两个缓冲。<br />
* 多次include 不同模块,最近 include 的模块作为直接超类向上延伸。<br />
* 模块也可以 include 模块,规则与类 include 模块一致,也是副本插入作为超类,作为目标类和原始超类之间新的超类。<br />
* Module prepend 例子:<br />
<br />
<pre><br />
module Professor<br />
def name<br />
"Prof. #{super}"<br />
end<br />
end<br />
class Mathematician<br />
attr_accessor :name<br />
prepend Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.name = 'Johann Carl Friedrich Gauss'<br />
<br />
p p.name<br />
<br />
</pre><br />
<br />
* prepend 虽然仍然会将 Professor 设置为 Mathematician 的新超类,但是同时会拷贝一份 Mathematician 作为 Mathematician 原生类(Origin class),将这个原生类作为 Professor 的超类,这就可以解释为什么 Professor#name 的 super 能调用到 Mathematician 的 name 方法。参考 http://ju.outofmemory.cn/entry/135588<br />
* 修改已被 include 模块,比如增加方法,所有 include 该模块的类都将包含新方法,因为共享 m_tbl 方法表,Ruby 在 include 的时候拷贝的只是 RClass struct,不拷贝底层的方法表 ,看下面例子:<br />
<br />
<pre><br />
module Professor<br />
def letcures ; end<br />
end<br />
<br />
class Mathematician<br />
attr_accessor :first_name<br />
attr_accessor :last_name<br />
<br />
include Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.first_name = 'hello'<br />
p.last_name = 'world'<br />
<br />
p p.methods.sort<br />
<br />
#open Professor, adds new method<br />
module Professor<br />
def classroom; end<br />
end<br />
<br />
p p.methods.sort<br />
<br />
</pre><br />
<br />
* 但是修改已被 include 模块中 include 的其他模块,不会影响插入到 include 类的已经被拷贝的模块副本,也就不会增加或者删除方法。<br />
* 当创建一个 class或者模块的时候 ,其实是新建了一层词法作用域,Ruby 用两个指针来标示: nd_clss ,指向当前作用域对应的模块或者类;nd_next 指向父层或者上下文的词法作用域。形成一个作用域链条。<br />
* 常量的查找跟方法的查找类似,只是方法的查找是通过祖先连(super) 来查找,而常量是通过迭代词法作用域链(nd_next)来查找。<br />
* Ruby 优先通过词法作用域来查找常量:<br />
<br />
<pre><br />
class SuperClass<br />
FIND_ME = "Found in Superclass"<br />
end<br />
<br />
module ParentLexicalScope<br />
FIND_ME = "Found in ParentLexicalScope"<br />
<br />
module ChildLexicalScope<br />
<br />
class SubClass < SuperClass<br />
p FIND_ME<br />
end<br />
end<br />
end<br />
<br />
</pre><br />
<br />
输出 "Found in ParentLexicalScope"<br />
<br />
* 真实的 Ruby 常量查找还需要加入 autoload 关键字:<br />
<br />
『检索词法作用域链 -> 为了个作用域的类检查 autoload -> 检索超类链 -> 为每个超类检查 autoload -> 调用 const_missing。』。<br />
<br />
* 关于 autoload http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html<br />
<br />
== 散列表 ==<br />
<br />
* ruby 的散列表 hash 解决哈希冲突还是经典的链表法,密度阈值设定为 5,超过就做 rehash。也就是 java hash 的所谓负载因子。<br />
* rehash 扩容的大小不是翻倍之类的算法,而是基于素数,总是将容器数目设置为一个素数表里的素数大小。这个考虑也是基于对哈希函数不够分布的担忧。<br />
* 比较元素通过 eq? 方法。<br />
* 默认哈希算法采用 murmur hash,这跟 clojure 是一样的。自定义对象作为 key,同样也可以选择自定义实现 hash 函数。一般都推荐使用默认。<br />
* Ruby 2.0 做了个优化,跟 clojure 一样,小于等于 6 个元素的 hash 直接组织成一个数组, clojure 里是少于等于8个就是 PersistentArrayMap,节省内存和提升效率。<br />
* RHash 转移到 internal.h<br />
<br />
<pre><br />
struct RHash {<br />
struct RBasic basic;<br />
struct st_table *ntbl; /* possibly 0 */<br />
int iter_lev;<br />
const VALUE ifnone;<br />
};<br />
</pre><br />
<br />
* ruby 2.4 又有一个大的变化,使用开放地址法替换了链表法,参考 https://bugs.ruby-lang.org/issues/12142,这个讨论非常有价值。<br />
<br />
<pre><br />
/* The original package implemented classic bucket-based hash tables<br />
with entries doubly linked for an access by their insertion order.<br />
To decrease pointer chasing and as a consequence to improve a data<br />
locality the current implementation is based on storing entries in<br />
an array and using hash tables with open addressing. The current<br />
entries are more compact in comparison with the original ones and<br />
this also improves the data locality.<br />
<br />
The hash table has two arrays called *bins* and *entries*.<br />
<br />
bins:<br />
-------<br />
| | entries array:<br />
|-------| --------------------------------<br />
| index | | | entry: | | |<br />
|-------| | | | | |<br />
| ... | | ... | hash | ... | ... |<br />
|-------| | | key | | |<br />
| empty | | | record | | |<br />
|-------| --------------------------------<br />
| ... | ^ ^<br />
|-------| |_ entries start |_ entries bound<br />
|deleted|<br />
-------<br />
<br />
o The entry array contains table entries in the same order as they<br />
were inserted.<br />
<br />
When the first entry is deleted, a variable containing index of<br />
the current first entry (*entries start*) is changed. In all<br />
other cases of the deletion, we just mark the entry as deleted by<br />
using a reserved hash value.<br />
<br />
Such organization of the entry storage makes operations of the<br />
table shift and the entries traversal very fast.<br />
<br />
o The bins provide access to the entries by their keys. The<br />
key hash is mapped to a bin containing *index* of the<br />
corresponding entry in the entry array.<br />
<br />
The bin array size is always power of two, it makes mapping very<br />
fast by using the corresponding lower bits of the hash.<br />
Generally it is not a good idea to ignore some part of the hash.<br />
But alternative approach is worse. For example, we could use a<br />
modulo operation for mapping and a prime number for the size of<br />
the bin array. Unfortunately, the modulo operation for big<br />
64-bit numbers are extremely slow (it takes more than 100 cycles<br />
on modern Intel CPUs).<br />
<br />
Still other bits of the hash value are used when the mapping<br />
results in a collision. In this case we use a secondary hash<br />
value which is a result of a function of the collision bin<br />
index and the original hash value. The function choice<br />
guarantees that we can traverse all bins and finally find the<br />
corresponding bin as after several iterations the function<br />
becomes a full cycle linear congruential generator because it<br />
satisfies requirements of the Hull-Dobell theorem.<br />
<br />
When an entry is removed from the table besides marking the<br />
hash in the corresponding entry described above, we also mark<br />
the bin by a special value in order to find entries which had<br />
a collision with the removed entries.<br />
<br />
There are two reserved values for the bins. One denotes an<br />
empty bin, another one denotes a bin for a deleted entry.<br />
<br />
o The length of the bin array is at least two times more than the<br />
entry array length. This keeps the table load factor healthy.<br />
The trigger of rebuilding the table is always a case when we can<br />
not insert an entry anymore at the entries bound. We could<br />
change the entries bound too in case of deletion but than we need<br />
a special code to count bins with corresponding deleted entries<br />
and reset the bin values when there are too many bins<br />
corresponding deleted entries<br />
<br />
Table rebuilding is done by creation of a new entry array and<br />
bins of an appropriate size. We also try to reuse the arrays<br />
in some cases by compacting the array and removing deleted<br />
entries.<br />
<br />
o To save memory very small tables have no allocated arrays<br />
bins. We use a linear search for an access by a key.<br />
<br />
o To save more memory we use 8-, 16-, 32- and 64- bit indexes in<br />
bins depending on the current hash table size.<br />
<br />
This implementation speeds up the Ruby hash table benchmarks in<br />
average by more 40% on Intel Haswell CPU.<br />
<br />
*/<br />
<br />
</pre><br />
<br />
=== 2.4 st_table 阅读心得 ===<br />
* st_features 定义了 table 的属性:<br />
** entry_power entries数组的大小,2的幂指数。<br />
** bin_power bins数组大小,同样是2的幂指数<br />
** size_ind 根据 table 大小,选择 bins 对应的元素的大小,可能是 8-bit, 16-bit etc。<br />
** bins_words bins按照word计算的大小。<br />
** 根据 SIZEOF_ST_INDEX_T 枚举了一堆 table 属性。<br />
* 因为 bins 的大小都是 2 的次幂,因此计算哈希值对应的 bin 可以直接用位运算:<br />
<pre><br />
/* Return mask for a bin index in table TAB. */<br />
static inline st_index_t<br />
bins_mask(const st_table *tab)<br />
{<br />
return get_bins_num(tab) - 1;<br />
}<br />
<br />
/* Return the index of table TAB bin corresponding to<br />
HASH_VALUE. */<br />
static inline st_index_t<br />
hash_bin(st_hash_t hash_value, st_table *tab)<br />
{<br />
return hash_value & bins_mask(tab);<br />
}<br />
</pre><br />
* 最小的 table 大小是 4, 由 MINIMAL_POWER2 决定。最大的 table 大小是 2 的 30 次方(非 8 位平台),8 位平台上是 2 的 62 次方。<br />
* 对于小于等于 16 个元素的 table,不创建 bins 数组,直接存储在 entries 数组,线性探测,无需进行 hash 计算和查找。<br />
* rebuild_table 可能有两种: compact 现有的,或者创建新的<br />
** 当已有 entries 数组的大小在现有元素大小的 2 倍到 4 倍(REBUILD_THRESHOLD)之间,或者元素数量小于 16 个,进入压缩流程。<br />
** 否则,进入新建流程。<br />
<br />
== 垃圾回收 ==</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-01-19T14:37:26Z
<p>Dennis zhuang:/* 垃圾回收 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。参考 http://ju.outofmemory.cn/entry/135587<br />
<br />
* Ruby 的方法缓存包含两层:全局的方法缓存,用于缓存接收者和实现类之间的映射,因为方法查找是要遍历整个继承链的,缓存可以加速这个调用。其次是内联方法缓存,缓存 Ruby 已经执行的已编译的 YARV 指令信息,这样可以避免查找,加速的原理和 clojure 的 direct linking 技术是一样的。无论是定义新方法、include 模块或者其他类似的操作, Ruby 都会去清空这两个缓冲。<br />
* 多次include 不同模块,最近 include 的模块作为直接超类向上延伸。<br />
* 模块也可以 include 模块,规则与类 include 模块一致,也是副本插入作为超类,作为目标类和原始超类之间新的超类。<br />
* Module prepend 例子:<br />
<br />
<pre><br />
module Professor<br />
def name<br />
"Prof. #{super}"<br />
end<br />
end<br />
class Mathematician<br />
attr_accessor :name<br />
prepend Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.name = 'Johann Carl Friedrich Gauss'<br />
<br />
p p.name<br />
<br />
</pre><br />
<br />
* prepend 虽然仍然会将 Professor 设置为 Mathematician 的新超类,但是同时会拷贝一份 Mathematician 作为 Mathematician 原生类(Origin class),将这个原生类作为 Professor 的超类,这就可以解释为什么 Professor#name 的 super 能调用到 Mathematician 的 name 方法。参考 http://ju.outofmemory.cn/entry/135588<br />
* 修改已被 include 模块,比如增加方法,所有 include 该模块的类都将包含新方法,因为共享 m_tbl 方法表,Ruby 在 include 的时候拷贝的只是 RClass struct,不拷贝底层的方法表 ,看下面例子:<br />
<br />
<pre><br />
module Professor<br />
def letcures ; end<br />
end<br />
<br />
class Mathematician<br />
attr_accessor :first_name<br />
attr_accessor :last_name<br />
<br />
include Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.first_name = 'hello'<br />
p.last_name = 'world'<br />
<br />
p p.methods.sort<br />
<br />
#open Professor, adds new method<br />
module Professor<br />
def classroom; end<br />
end<br />
<br />
p p.methods.sort<br />
<br />
</pre><br />
<br />
* 但是修改已被 include 模块中 include 的其他模块,不会影响插入到 include 类的已经被拷贝的模块副本,也就不会增加或者删除方法。<br />
* 当创建一个 class或者模块的时候 ,其实是新建了一层词法作用域,Ruby 用两个指针来标示: nd_clss ,指向当前作用域对应的模块或者类;nd_next 指向父层或者上下文的词法作用域。形成一个作用域链条。<br />
* 常量的查找跟方法的查找类似,只是方法的查找是通过祖先连(super) 来查找,而常量是通过迭代词法作用域链(nd_next)来查找。<br />
* Ruby 优先通过词法作用域来查找常量:<br />
<br />
<pre><br />
class SuperClass<br />
FIND_ME = "Found in Superclass"<br />
end<br />
<br />
module ParentLexicalScope<br />
FIND_ME = "Found in ParentLexicalScope"<br />
<br />
module ChildLexicalScope<br />
<br />
class SubClass < SuperClass<br />
p FIND_ME<br />
end<br />
end<br />
end<br />
<br />
</pre><br />
<br />
输出 "Found in ParentLexicalScope"<br />
<br />
* 真实的 Ruby 常量查找还需要加入 autoload 关键字:<br />
<br />
『检索词法作用域链 -> 为了个作用域的类检查 autoload -> 检索超类链 -> 为每个超类检查 autoload -> 调用 const_missing。』。<br />
<br />
* 关于 autoload http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html<br />
<br />
== 散列表 ==<br />
<br />
* ruby 的散列表 hash 解决哈希冲突还是经典的链表法,密度阈值设定为 5,超过就做 rehash。也就是 java hash 的所谓负载因子。<br />
* rehash 扩容的大小不是翻倍之类的算法,而是基于素数,总是将容器数目设置为一个素数表里的素数大小。这个考虑也是基于对哈希函数不够分布的担忧。<br />
* 比较元素通过 eq? 方法。<br />
* 默认哈希算法采用 murmur hash,这跟 clojure 是一样的。自定义对象作为 key,同样也可以选择自定义实现 hash 函数。一般都推荐使用默认。<br />
* Ruby 2.0 做了个优化,跟 clojure 一样,小于等于 6 个元素的 hash 直接组织成一个数组, clojure 里是少于等于8个就是 PersistentArrayMap,节省内存和提升效率。<br />
* RHash 转移到 internal.h<br />
<br />
<pre><br />
struct RHash {<br />
struct RBasic basic;<br />
struct st_table *ntbl; /* possibly 0 */<br />
int iter_lev;<br />
const VALUE ifnone;<br />
};<br />
</pre><br />
<br />
* ruby 2.4 又有一个大的变化,使用开放地址法替换了链表法,参考 https://bugs.ruby-lang.org/issues/12142,这个讨论非常有价值。<br />
<br />
<pre><br />
/* The original package implemented classic bucket-based hash tables<br />
with entries doubly linked for an access by their insertion order.<br />
To decrease pointer chasing and as a consequence to improve a data<br />
locality the current implementation is based on storing entries in<br />
an array and using hash tables with open addressing. The current<br />
entries are more compact in comparison with the original ones and<br />
this also improves the data locality.<br />
<br />
The hash table has two arrays called *bins* and *entries*.<br />
<br />
bins:<br />
-------<br />
| | entries array:<br />
|-------| --------------------------------<br />
| index | | | entry: | | |<br />
|-------| | | | | |<br />
| ... | | ... | hash | ... | ... |<br />
|-------| | | key | | |<br />
| empty | | | record | | |<br />
|-------| --------------------------------<br />
| ... | ^ ^<br />
|-------| |_ entries start |_ entries bound<br />
|deleted|<br />
-------<br />
<br />
o The entry array contains table entries in the same order as they<br />
were inserted.<br />
<br />
When the first entry is deleted, a variable containing index of<br />
the current first entry (*entries start*) is changed. In all<br />
other cases of the deletion, we just mark the entry as deleted by<br />
using a reserved hash value.<br />
<br />
Such organization of the entry storage makes operations of the<br />
table shift and the entries traversal very fast.<br />
<br />
o The bins provide access to the entries by their keys. The<br />
key hash is mapped to a bin containing *index* of the<br />
corresponding entry in the entry array.<br />
<br />
The bin array size is always power of two, it makes mapping very<br />
fast by using the corresponding lower bits of the hash.<br />
Generally it is not a good idea to ignore some part of the hash.<br />
But alternative approach is worse. For example, we could use a<br />
modulo operation for mapping and a prime number for the size of<br />
the bin array. Unfortunately, the modulo operation for big<br />
64-bit numbers are extremely slow (it takes more than 100 cycles<br />
on modern Intel CPUs).<br />
<br />
Still other bits of the hash value are used when the mapping<br />
results in a collision. In this case we use a secondary hash<br />
value which is a result of a function of the collision bin<br />
index and the original hash value. The function choice<br />
guarantees that we can traverse all bins and finally find the<br />
corresponding bin as after several iterations the function<br />
becomes a full cycle linear congruential generator because it<br />
satisfies requirements of the Hull-Dobell theorem.<br />
<br />
When an entry is removed from the table besides marking the<br />
hash in the corresponding entry described above, we also mark<br />
the bin by a special value in order to find entries which had<br />
a collision with the removed entries.<br />
<br />
There are two reserved values for the bins. One denotes an<br />
empty bin, another one denotes a bin for a deleted entry.<br />
<br />
o The length of the bin array is at least two times more than the<br />
entry array length. This keeps the table load factor healthy.<br />
The trigger of rebuilding the table is always a case when we can<br />
not insert an entry anymore at the entries bound. We could<br />
change the entries bound too in case of deletion but than we need<br />
a special code to count bins with corresponding deleted entries<br />
and reset the bin values when there are too many bins<br />
corresponding deleted entries<br />
<br />
Table rebuilding is done by creation of a new entry array and<br />
bins of an appropriate size. We also try to reuse the arrays<br />
in some cases by compacting the array and removing deleted<br />
entries.<br />
<br />
o To save memory very small tables have no allocated arrays<br />
bins. We use a linear search for an access by a key.<br />
<br />
o To save more memory we use 8-, 16-, 32- and 64- bit indexes in<br />
bins depending on the current hash table size.<br />
<br />
This implementation speeds up the Ruby hash table benchmarks in<br />
average by more 40% on Intel Haswell CPU.<br />
<br />
*/<br />
<br />
</pre><br />
<br />
=== 2.4 st_table 阅读心得 ===<br />
* st_features 定义了 table 的属性:<br />
** entry_power entries数组的大小,2的幂指数。<br />
** bin_power bins数组大小,同样是2的幂指数<br />
** size_ind 根据 table 大小,选择 bins 对应的元素的大小,可能是 8-bit, 16-bit etc。<br />
** bins_words bins按照word计算的大小。<br />
* 对于小于等于 16 个元素的 table,不创建 bins 数组,直接存储在 entries 数组,线性探测,无需进行 hash 计算和查找。<br />
* rebuild_table 可能有两种: compact 现有的,或者创建新的<br />
** 当已有 entries 数组的大小在现有元素大小的 2 倍到 4 倍(REBUILD_THRESHOLD)之间,或者元素数量小于 16 个,进入压缩流程。<br />
** 否则,进入新建流程。<br />
<br />
== 垃圾回收 ==</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-01-19T14:00:40Z
<p>Dennis zhuang:/* 散列表 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。参考 http://ju.outofmemory.cn/entry/135587<br />
<br />
* Ruby 的方法缓存包含两层:全局的方法缓存,用于缓存接收者和实现类之间的映射,因为方法查找是要遍历整个继承链的,缓存可以加速这个调用。其次是内联方法缓存,缓存 Ruby 已经执行的已编译的 YARV 指令信息,这样可以避免查找,加速的原理和 clojure 的 direct linking 技术是一样的。无论是定义新方法、include 模块或者其他类似的操作, Ruby 都会去清空这两个缓冲。<br />
* 多次include 不同模块,最近 include 的模块作为直接超类向上延伸。<br />
* 模块也可以 include 模块,规则与类 include 模块一致,也是副本插入作为超类,作为目标类和原始超类之间新的超类。<br />
* Module prepend 例子:<br />
<br />
<pre><br />
module Professor<br />
def name<br />
"Prof. #{super}"<br />
end<br />
end<br />
class Mathematician<br />
attr_accessor :name<br />
prepend Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.name = 'Johann Carl Friedrich Gauss'<br />
<br />
p p.name<br />
<br />
</pre><br />
<br />
* prepend 虽然仍然会将 Professor 设置为 Mathematician 的新超类,但是同时会拷贝一份 Mathematician 作为 Mathematician 原生类(Origin class),将这个原生类作为 Professor 的超类,这就可以解释为什么 Professor#name 的 super 能调用到 Mathematician 的 name 方法。参考 http://ju.outofmemory.cn/entry/135588<br />
* 修改已被 include 模块,比如增加方法,所有 include 该模块的类都将包含新方法,因为共享 m_tbl 方法表,Ruby 在 include 的时候拷贝的只是 RClass struct,不拷贝底层的方法表 ,看下面例子:<br />
<br />
<pre><br />
module Professor<br />
def letcures ; end<br />
end<br />
<br />
class Mathematician<br />
attr_accessor :first_name<br />
attr_accessor :last_name<br />
<br />
include Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.first_name = 'hello'<br />
p.last_name = 'world'<br />
<br />
p p.methods.sort<br />
<br />
#open Professor, adds new method<br />
module Professor<br />
def classroom; end<br />
end<br />
<br />
p p.methods.sort<br />
<br />
</pre><br />
<br />
* 但是修改已被 include 模块中 include 的其他模块,不会影响插入到 include 类的已经被拷贝的模块副本,也就不会增加或者删除方法。<br />
* 当创建一个 class或者模块的时候 ,其实是新建了一层词法作用域,Ruby 用两个指针来标示: nd_clss ,指向当前作用域对应的模块或者类;nd_next 指向父层或者上下文的词法作用域。形成一个作用域链条。<br />
* 常量的查找跟方法的查找类似,只是方法的查找是通过祖先连(super) 来查找,而常量是通过迭代词法作用域链(nd_next)来查找。<br />
* Ruby 优先通过词法作用域来查找常量:<br />
<br />
<pre><br />
class SuperClass<br />
FIND_ME = "Found in Superclass"<br />
end<br />
<br />
module ParentLexicalScope<br />
FIND_ME = "Found in ParentLexicalScope"<br />
<br />
module ChildLexicalScope<br />
<br />
class SubClass < SuperClass<br />
p FIND_ME<br />
end<br />
end<br />
end<br />
<br />
</pre><br />
<br />
输出 "Found in ParentLexicalScope"<br />
<br />
* 真实的 Ruby 常量查找还需要加入 autoload 关键字:<br />
<br />
『检索词法作用域链 -> 为了个作用域的类检查 autoload -> 检索超类链 -> 为每个超类检查 autoload -> 调用 const_missing。』。<br />
<br />
* 关于 autoload http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html<br />
<br />
== 散列表 ==<br />
<br />
* ruby 的散列表 hash 解决哈希冲突还是经典的链表法,密度阈值设定为 5,超过就做 rehash。也就是 java hash 的所谓负载因子。<br />
* rehash 扩容的大小不是翻倍之类的算法,而是基于素数,总是将容器数目设置为一个素数表里的素数大小。这个考虑也是基于对哈希函数不够分布的担忧。<br />
* 比较元素通过 eq? 方法。<br />
* 默认哈希算法采用 murmur hash,这跟 clojure 是一样的。自定义对象作为 key,同样也可以选择自定义实现 hash 函数。一般都推荐使用默认。<br />
* Ruby 2.0 做了个优化,跟 clojure 一样,小于等于 6 个元素的 hash 直接组织成一个数组, clojure 里是少于等于8个就是 PersistentArrayMap,节省内存和提升效率。<br />
* RHash 转移到 internal.h<br />
<br />
<pre><br />
struct RHash {<br />
struct RBasic basic;<br />
struct st_table *ntbl; /* possibly 0 */<br />
int iter_lev;<br />
const VALUE ifnone;<br />
};<br />
</pre><br />
<br />
* ruby 2.4 又有一个大的变化,使用开放地址法替换了链表法,参考 https://bugs.ruby-lang.org/issues/12142,这个讨论非常有价值。<br />
<br />
<pre><br />
/* The original package implemented classic bucket-based hash tables<br />
with entries doubly linked for an access by their insertion order.<br />
To decrease pointer chasing and as a consequence to improve a data<br />
locality the current implementation is based on storing entries in<br />
an array and using hash tables with open addressing. The current<br />
entries are more compact in comparison with the original ones and<br />
this also improves the data locality.<br />
<br />
The hash table has two arrays called *bins* and *entries*.<br />
<br />
bins:<br />
-------<br />
| | entries array:<br />
|-------| --------------------------------<br />
| index | | | entry: | | |<br />
|-------| | | | | |<br />
| ... | | ... | hash | ... | ... |<br />
|-------| | | key | | |<br />
| empty | | | record | | |<br />
|-------| --------------------------------<br />
| ... | ^ ^<br />
|-------| |_ entries start |_ entries bound<br />
|deleted|<br />
-------<br />
<br />
o The entry array contains table entries in the same order as they<br />
were inserted.<br />
<br />
When the first entry is deleted, a variable containing index of<br />
the current first entry (*entries start*) is changed. In all<br />
other cases of the deletion, we just mark the entry as deleted by<br />
using a reserved hash value.<br />
<br />
Such organization of the entry storage makes operations of the<br />
table shift and the entries traversal very fast.<br />
<br />
o The bins provide access to the entries by their keys. The<br />
key hash is mapped to a bin containing *index* of the<br />
corresponding entry in the entry array.<br />
<br />
The bin array size is always power of two, it makes mapping very<br />
fast by using the corresponding lower bits of the hash.<br />
Generally it is not a good idea to ignore some part of the hash.<br />
But alternative approach is worse. For example, we could use a<br />
modulo operation for mapping and a prime number for the size of<br />
the bin array. Unfortunately, the modulo operation for big<br />
64-bit numbers are extremely slow (it takes more than 100 cycles<br />
on modern Intel CPUs).<br />
<br />
Still other bits of the hash value are used when the mapping<br />
results in a collision. In this case we use a secondary hash<br />
value which is a result of a function of the collision bin<br />
index and the original hash value. The function choice<br />
guarantees that we can traverse all bins and finally find the<br />
corresponding bin as after several iterations the function<br />
becomes a full cycle linear congruential generator because it<br />
satisfies requirements of the Hull-Dobell theorem.<br />
<br />
When an entry is removed from the table besides marking the<br />
hash in the corresponding entry described above, we also mark<br />
the bin by a special value in order to find entries which had<br />
a collision with the removed entries.<br />
<br />
There are two reserved values for the bins. One denotes an<br />
empty bin, another one denotes a bin for a deleted entry.<br />
<br />
o The length of the bin array is at least two times more than the<br />
entry array length. This keeps the table load factor healthy.<br />
The trigger of rebuilding the table is always a case when we can<br />
not insert an entry anymore at the entries bound. We could<br />
change the entries bound too in case of deletion but than we need<br />
a special code to count bins with corresponding deleted entries<br />
and reset the bin values when there are too many bins<br />
corresponding deleted entries<br />
<br />
Table rebuilding is done by creation of a new entry array and<br />
bins of an appropriate size. We also try to reuse the arrays<br />
in some cases by compacting the array and removing deleted<br />
entries.<br />
<br />
o To save memory very small tables have no allocated arrays<br />
bins. We use a linear search for an access by a key.<br />
<br />
o To save more memory we use 8-, 16-, 32- and 64- bit indexes in<br />
bins depending on the current hash table size.<br />
<br />
This implementation speeds up the Ruby hash table benchmarks in<br />
average by more 40% on Intel Haswell CPU.<br />
<br />
*/<br />
<br />
</pre><br />
<br />
== 垃圾回收 ==</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-01-17T14:44:32Z
<p>Dennis zhuang:/* 方法查找和常量查找 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。参考 http://ju.outofmemory.cn/entry/135587<br />
<br />
* Ruby 的方法缓存包含两层:全局的方法缓存,用于缓存接收者和实现类之间的映射,因为方法查找是要遍历整个继承链的,缓存可以加速这个调用。其次是内联方法缓存,缓存 Ruby 已经执行的已编译的 YARV 指令信息,这样可以避免查找,加速的原理和 clojure 的 direct linking 技术是一样的。无论是定义新方法、include 模块或者其他类似的操作, Ruby 都会去清空这两个缓冲。<br />
* 多次include 不同模块,最近 include 的模块作为直接超类向上延伸。<br />
* 模块也可以 include 模块,规则与类 include 模块一致,也是副本插入作为超类,作为目标类和原始超类之间新的超类。<br />
* Module prepend 例子:<br />
<br />
<pre><br />
module Professor<br />
def name<br />
"Prof. #{super}"<br />
end<br />
end<br />
class Mathematician<br />
attr_accessor :name<br />
prepend Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.name = 'Johann Carl Friedrich Gauss'<br />
<br />
p p.name<br />
<br />
</pre><br />
<br />
* prepend 虽然仍然会将 Professor 设置为 Mathematician 的新超类,但是同时会拷贝一份 Mathematician 作为 Mathematician 原生类(Origin class),将这个原生类作为 Professor 的超类,这就可以解释为什么 Professor#name 的 super 能调用到 Mathematician 的 name 方法。参考 http://ju.outofmemory.cn/entry/135588<br />
* 修改已被 include 模块,比如增加方法,所有 include 该模块的类都将包含新方法,因为共享 m_tbl 方法表,Ruby 在 include 的时候拷贝的只是 RClass struct,不拷贝底层的方法表 ,看下面例子:<br />
<br />
<pre><br />
module Professor<br />
def letcures ; end<br />
end<br />
<br />
class Mathematician<br />
attr_accessor :first_name<br />
attr_accessor :last_name<br />
<br />
include Professor<br />
end<br />
<br />
p = Mathematician.new<br />
p.first_name = 'hello'<br />
p.last_name = 'world'<br />
<br />
p p.methods.sort<br />
<br />
#open Professor, adds new method<br />
module Professor<br />
def classroom; end<br />
end<br />
<br />
p p.methods.sort<br />
<br />
</pre><br />
<br />
* 但是修改已被 include 模块中 include 的其他模块,不会影响插入到 include 类的已经被拷贝的模块副本,也就不会增加或者删除方法。<br />
* 当创建一个 class或者模块的时候 ,其实是新建了一层词法作用域,Ruby 用两个指针来标示: nd_clss ,指向当前作用域对应的模块或者类;nd_next 指向父层或者上下文的词法作用域。形成一个作用域链条。<br />
* 常量的查找跟方法的查找类似,只是方法的查找是通过祖先连(super) 来查找,而常量是通过迭代词法作用域链(nd_next)来查找。<br />
* Ruby 优先通过词法作用域来查找常量:<br />
<br />
<pre><br />
class SuperClass<br />
FIND_ME = "Found in Superclass"<br />
end<br />
<br />
module ParentLexicalScope<br />
FIND_ME = "Found in ParentLexicalScope"<br />
<br />
module ChildLexicalScope<br />
<br />
class SubClass < SuperClass<br />
p FIND_ME<br />
end<br />
end<br />
end<br />
<br />
</pre><br />
<br />
输出 "Found in ParentLexicalScope"<br />
<br />
* 真实的 Ruby 常量查找还需要加入 autoload 关键字:<br />
<br />
『检索词法作用域链 -> 为了个作用域的类检查 autoload -> 检索超类链 -> 为每个超类检查 autoload -> 调用 const_missing。』。<br />
<br />
* 关于 autoload http://www.rubyinside.com/ruby-techniques-revealed-autoload-1652.html<br />
<br />
== 散列表 ==<br />
<br />
== 垃圾回收 ==</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-01-17T13:50:18Z
<p>Dennis zhuang:/* 方法查找和常量查找 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
在代码最后,如果发现方法有变化,就清空方法缓存,如果常量有变化,就清空常量缓存。<br />
<br />
== 散列表 ==<br />
<br />
== 垃圾回收 ==</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=Ruby_Under_a_Microscope
Ruby Under a Microscope
2017-01-17T13:46:37Z
<p>Dennis zhuang:/* 方法查找和常量查找 */</p>
<hr />
<div><br />
<br />
== 分词与语法解析 ==<br />
<br />
* 使用 Ripper 输出 lex 结果。<br />
<br />
<pre><br />
require 'ripper'<br />
require 'pp'<br />
#ripper is not parser, it can't find error.<br />
code = <<STR<br />
10.times do |n|<br />
puts n<br />
end<br />
STR<br />
<br />
puts code<br />
pp Ripper.lex(code)<br />
</pre><br />
<br />
* Ripper.sexp 输出 parse 结果,也可以使用命令行 ruby --dump parsetree xxxx.rb 得到。前者是 Ripper 的 AST 展示格式,后者是实际内部的 c 语言 node 节点信息。<br />
* Ruby 使用手写的 tokenizer ,以及 bison 写的 parser —— [https://github.com/ruby/ruby/blob/510f0ec86912e31babaadf1f66bf2a82351c1359/parse.y parse.y] ,bison生成的解释器是 [https://en.wikipedia.org/wiki/LALR_parser LALR Parser]。<br />
<br />
== 编译 ==<br />
<br />
* Ruby 1.8 没有编译器, Ruby 1.9 之后引入了 YARV( yet another ruby vm) 中间指令。但是 Ruby 并没有独立的编译器,而是在运行时动态编译成字节码,并交给 VM 解释执行。很可惜, Ruby 还是没有 JIT,将字节码编译成本地机器码。但是从测试来看, 1.9 之后的性能,已经远比 1.8 高(简单测试是 4.25 倍左右), 1.8 还是原始的解释执行 AST 的方式。<br />
* 编译的过程本质是遍历 AST ,然后生成 YARV 字节码的过程,具体参考 https://github.com/ruby/ruby/blob/trunk/compile.c 中的 iseq_compile_each 函数,一个大的 switch 派发。<br />
* NODE_SCOPE 表示开始一个新的作用域,作用域绑定着一个本地表 local table,类似 JVM 里的局部变量区,参数和局部变量的信息会放在这里。<br />
* 查看 YARV 字节码:<br />
<br />
<pre><br />
code = <<END<br />
10.times do |n|<br />
puts n<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
</pre><br />
<br />
输出<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
0000 trace 1 ( 1)<br />
0002 putobject 10<br />
0004 send <callinfo!mid:times, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0002 ed: 0010 sp: 0000 cont: 0002<br />
| catch type: next st: 0002 ed: 0010 sp: 0000 cont: 0010<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] n<Arg><br />
0000 trace 256 ( 1)<br />
0002 trace 1 ( 2)<br />
0004 putself<br />
0005 getlocal_OP__WC__0 2<br />
0007 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0010 trace 512 ( 3)<br />
0012 leave ( 2)<br />
</pre><br />
<br />
其中的 local table 就是本地表,<code><callinfo!mid:times, argc:0>, <callcache>, block in <compiled></code> 这里表示为 10.times 传递了一个 Block,它的指令在下面。<br />
<br />
* 此外,想函数的默认参数、命名参数都是通过生成额外的指令来支持,前者就是加入判断,后者是引入匿名的 hash 表。<br />
<br />
== YARV 执行代码 ==<br />
<br />
* 整体上, YARV 跟 JVM 的构造机器类似。 YARV 也是有一个调用栈,每个栈帧 rb_control_frame_t 包含 sp( stack pointer,指向栈顶), pc(程序计数器,当前指令地址),self(接收者) 和 type (节点类型)等信息。CFP (current frame pointer) 指向当前的 rb_control_frame_t。调用就是压入和弹出栈帧,栈帧内部维护操作数栈,pc 指向指令地址,对操作数和接收者进行入栈出栈操作,根据指令求值。YARV 也被称为是双堆栈虚拟机。<br />
* 所有 YARV 指令定义在 https://github.com/ruby/ruby/blob/bd2fd73196bbff7dc5349c624342e212c09d174e/insns.def,最终经过 Miniruby 转成 vm.inc 的 c 语言代码。<br />
* 指令基本格式<br />
<br />
<pre><br />
instruction comment<br />
@c: category<br />
@e: english description<br />
@j: japanese description<br />
instruction form:<br />
DEFINE_INSN<br />
instruction_name<br />
(instruction_operands, ..)<br />
(pop_values, ..)<br />
(return value)<br />
{<br />
.. // insn body<br />
}<br />
<br />
DEFINE_INSN<br />
getlocal<br />
(lindex_t idx, rb_num_t level)<br />
()<br />
(VALUE val)<br />
{<br />
int i, lev = (int)level;<br />
const VALUE *ep = GET_EP();<br />
<br />
/* optimized insns generated for level == (0|1) in defs/opt_operand.def */<br />
for (i = 0; i < lev; i++) {<br />
ep = GET_PREV_EP(ep);<br />
}<br />
val = *(ep - idx);<br />
}<br />
</pre><br />
<br />
* 本地变量的访问,通过 getlocal 和 setlocal 指令,当 CFP 变化的时候,为了访问本栈帧之外的 local 变量, YARV 还引入了一个叫 EP( environment pointer) 的指针,它被设置为 SP-1。 栈帧之间的 EP 形成了一种层次结构(其实就是嵌套作用域),通过 EP 的移动来访问外部环境的本地变量。<br />
* 内部栈还有两个特殊栈帧 special 和 svar/cref, special 用于保存传递了 Block 代码块的指针,指向代码块所在的栈帧,让 EP 可以找到正确的栈帧。后者 svar 用于保存特殊变量,$ 开头的一些特殊变量,特别是跟正则相关的,比如 $&, $~ 等。而 cref 用于标示是否要在一个新的词法作用域内(lexical scope)执行。 Ruby 中开启新的词法作用域的只有:使用class关键字定义一个类;使用module 定义一个模块;使用def关键字定义一个方法。而 Block 是没有的。这一块在 ruby 元编程里有详细描述。<br />
<br />
== 控制结构和方法调度 ==<br />
<br />
<br />
* if 语句本质上是使用 branchunless 或者 branchif 根据 test 的计算结果为 true/false 来决定跳转到哪个代码分支继续执行。<br />
* 跨作用域的跳转(比如 break 跳转到父作用域),则是使用 throw 指令 + 捕获表实现,向下找到最近的捕获表的 break 指针,然后充值 pc 和 ep 指针,从指针后的代码开始执行。rescue、ensure、retry、redo 和 next 的实现与此类似。<br />
<br />
* for 只是 each 的封装,查看<br />
<br />
<pre><br />
code = <<END<br />
for i in 0..5<br />
puts i<br />
end<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
输出:<br />
<br />
<pre><br />
== disasm: #<ISeq:<compiled>@<compiled>>================================<br />
== catch table<br />
| catch type: break st: 0002 ed: 0008 sp: 0000 cont: 0008<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] i<br />
0000 trace 1 ( 1)<br />
0002 putobject 0..5<br />
0004 send <callinfo!mid:each, argc:0>, <callcache>, block in <compiled><br />
0008 leave<br />
== disasm: #<ISeq:block in <compiled>@<compiled>>=======================<br />
== catch table<br />
| catch type: redo st: 0006 ed: 0014 sp: 0000 cont: 0006<br />
| catch type: next st: 0006 ed: 0014 sp: 0000 cont: 0014<br />
|------------------------------------------------------------------------<br />
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])<br />
[ 2] ?<Arg><br />
0000 getlocal_OP__WC__0 2 ( 3)<br />
0002 setlocal_OP__WC__1 2 ( 1)<br />
0004 trace 256<br />
0006 trace 1 ( 2)<br />
0008 putself<br />
0009 getlocal_OP__WC__1 2<br />
0011 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache><br />
0014 trace 512 ( 3)<br />
0016 leave<br />
</pre><br />
<br />
可以看到是调用了 0..5 的 each 方法,然后将 block 参数拷贝给了局部变量 i。<br />
<br />
* send 是最核心和最复杂的控制结构, ruby 有 11 种方法类型<br />
* ISEQ 是普通方法, CFUNC 是 c 语言编写的代码,都是 ruby 的内部实现。ATTRSET 是 attr_writer, IVAR 是 attr_reader, BMETHOD 表示 define_method 传入的 proc 对象定义的方法,UNDEF 用于移除方法, MISSING 是方法不存在的时候调用,其他等等。<br />
* 对于 attr_reader 和 attr_writer, ruby 内部做了优化,不会创建新的栈帧,因为方法非常简短并且不会出错,也就需要错误的堆栈信息。内部使用 c 语言实现的 vm_setivar 和 vm_getivar 快速调用。<br />
* 命名参数本质是创建了一个 hash 来包装,如果 hash.key? 返回 false,也就是不存在,就使用默认值。具体可以看下面这段代码的输出:<br />
<br />
<pre><br />
code = <<END<br />
def add_two(a: 2, b: 3)<br />
a + b<br />
end<br />
<br />
puts add_two(1, 1)<br />
END<br />
<br />
puts RubyVM::InstructionSequence.compile(code).disasm<br />
<br />
</pre><br />
<br />
== 对象与类 ==<br />
<br />
=== Ruby 对象 RObject ===<br />
<br />
* 在 include/ruby/ruby.h 中定义:<br />
<br />
<pre><br />
struct RBasic {<br />
VALUE flags;<br />
const VALUE klass;<br />
}<br />
#ifdef __GNUC__<br />
__attribute__((aligned(sizeof(VALUE))))<br />
#endif<br />
;<br />
<br />
struct RObject {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
uint32_t numiv;<br />
VALUE *ivptr;<br />
void *iv_index_tbl; /* shortcut for RCLASS_IV_INDEX_TBL(rb_obj_class(obj)) */<br />
} heap;<br />
VALUE ary[ROBJECT_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
</pre><br />
<br />
其中:<br />
<br />
RBasic 里的 class 指针指向了 RClass,也就是对象所属的 class。<br />
flags 用于存储内部专用的各种标志位。<br />
numiv 表示实例变量数目<br />
ivptr 实例变量数组<br />
iv_index_tbl 指向散列表的指针,该散列表保存了实例变量名(ID) 及其在 ivptr 数组中位置的映射,这些散列值存储在 RClass 的结构体中,该指针只是一个简单的缓存来加速访问。<br />
<br />
* 基本类型对象,比如字符串、数组、正则表达式等有单独的结构体,例如 RString 、RArray 和 RRegexp 等等,每个实例内部同样有 basic 指针。<br />
<br />
<pre><br />
struct RString {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
char *ptr;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
} heap;<br />
char ary[RSTRING_EMBED_LEN_MAX + 1];<br />
} as;<br />
};<br />
<br />
struct RArray {<br />
struct RBasic basic;<br />
union {<br />
struct {<br />
long len;<br />
union {<br />
long capa;<br />
VALUE shared;<br />
} aux;<br />
const VALUE *ptr;<br />
} heap;<br />
const VALUE ary[RARRAY_EMBED_LEN_MAX];<br />
} as;<br />
};<br />
<br />
</pre><br />
<br />
等等。但是数字、符号等一些简单的立即值,没有使用任何结构体,而是直接将它们放到 VALUE 内,并且留前面几个 bit 位来标记类型:<br />
<br />
<pre><br />
[ Integer value | Flags ]<br />
</pre><br />
<br />
基本类型对象也有实例变量,但是单独保存在 generic_iv_tbl 的特殊散列表里。<br />
<br />
=== RClass 结构体 ===<br />
<br />
* Ruby 2.3.0 开始将 RClass 从 include/ruby/ruby.h 迁移到了 internal.h 中,为了信息隐藏:<br />
<br />
<pre><br />
struct rb_classext_struct {<br />
struct st_table *iv_index_tbl;<br />
struct st_table *iv_tbl;<br />
struct rb_id_table *const_tbl;<br />
struct rb_id_table *callable_m_tbl;<br />
rb_subclass_entry_t *subclasses;<br />
rb_subclass_entry_t **parent_subclasses;<br />
/**<br />
* In the case that this is an `ICLASS`, `module_subclasses` points to the link<br />
* in the module's `subclasses` list that indicates that the klass has been<br />
* included. Hopefully that makes sense.<br />
*/<br />
rb_subclass_entry_t **module_subclasses;<br />
rb_serial_t class_serial;<br />
const VALUE origin_;<br />
VALUE refined_class;<br />
rb_alloc_func_t allocator;<br />
};<br />
<br />
struct RClass {<br />
struct RBasic basic;<br />
VALUE super;<br />
rb_classext_t *ptr;<br />
struct rb_id_table *m_tbl;<br />
};<br />
<br />
</pre><br />
<br />
其中:<br />
m_table 是方法的散列表,以方法名或者 ID 为键,以每个方法的定义的指针为值。<br />
iv_index_tbl 是属性名散列表,是实例变量名和 RObject 实例变量数组属性值索引位置的映射。RObject 里有 iv_index_tbl 缓存指向这个散列表。<br />
iv_tbl 类级别的实例变量和类变量,包括他们的名字和值。<br />
const_tbl 常量散列表。<br />
origin 用于实现 Module#prepend 特性。<br />
allocator 用于分配内存。<br />
super 指向超类 RClass 的指针。<br />
<br />
* 类变量(@@)和类的实例变量(@)的区别:类变量在所有子类中共用;类实例变量在该类和子类中创建各自独自的值。但是他们都保存在 iv_tbl,通过名字区分。<br />
* 类变量的查找顺序是会从该类和超类找起来,找到最高层的的超类的变量副本。而类的实例变量只在当前类查找。<br />
* 类的类方法 (self.xxx) 是保存在元类 metaclass, 类的 RBasic 里 klass 指向的就是它的元类。一个小实验:<br />
<br />
<pre><br />
irb(main):001:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 912<br />
irb(main):002:0> class Test end<br />
=> nil<br />
irb(main):003:0> ObjectSpace.count_objects[:T_CLASS]<br />
=> 914<br />
</pre><br />
<br />
== 方法查找和常量查找 ==<br />
<br />
* moule 也是 class,但是移除了 iv_index_tbl, allocator 等,因为模块没有对象级别的属性和方法。<br />
* include 一个模块,本质上是拷贝该模块的 RClass 结构体形成一个新副本,然后作为类的新超类,模块副本加入了祖先继承链。include 的核心逻辑在 ruby.c 里的 rb_include_module 和 include_modules_at 函数里。复制发生在 rb_include_class_new 函数。<br />
<pre><br />
VALUE<br />
rb_include_class_new(VALUE module, VALUE super)<br />
{<br />
VALUE klass = class_alloc(T_ICLASS, rb_cClass);<br />
<br />
if (BUILTIN_TYPE(module) == T_ICLASS) {<br />
module = RBASIC(module)->klass;<br />
}<br />
if (!RCLASS_IV_TBL(module)) {<br />
RCLASS_IV_TBL(module) = st_init_numtable();<br />
}<br />
if (!RCLASS_CONST_TBL(module)) {<br />
RCLASS_CONST_TBL(module) = rb_id_table_create(0);<br />
}<br />
RCLASS_IV_TBL(klass) = RCLASS_IV_TBL(module);<br />
RCLASS_CONST_TBL(klass) = RCLASS_CONST_TBL(module);<br />
<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(klass)) =<br />
RCLASS_M_TBL(OBJ_WB_UNPROTECT(RCLASS_ORIGIN(module))); /* TODO: unprotected? */<br />
<br />
RCLASS_SET_SUPER(klass, super);<br />
if (RB_TYPE_P(module, T_ICLASS)) {<br />
RBASIC_SET_CLASS(klass, RBASIC(module)->klass);<br />
}<br />
else {<br />
RBASIC_SET_CLASS(klass, module);<br />
}<br />
OBJ_INFECT(klass, module);<br />
OBJ_INFECT(klass, super);<br />
<br />
return (VALUE)klass;<br />
}<br />
<br />
static int include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super);<br />
<br />
void<br />
rb_include_module(VALUE klass, VALUE module)<br />
{<br />
int changed = 0;<br />
<br />
rb_frozen_class_p(klass);<br />
Check_Type(module, T_MODULE);<br />
OBJ_INFECT(klass, module);<br />
<br />
changed = include_modules_at(klass, RCLASS_ORIGIN(klass), module, TRUE);<br />
if (changed < 0)<br />
rb_raise(rb_eArgError, "cyclic include detected");<br />
}<br />
<br />
static enum rb_id_table_iterator_result<br />
add_refined_method_entry_i(ID key, VALUE value, void *data)<br />
{<br />
rb_add_refined_method_entry((VALUE)data, key);<br />
return ID_TABLE_CONTINUE;<br />
}<br />
<br />
static int<br />
include_modules_at(const VALUE klass, VALUE c, VALUE module, int search_super)<br />
{<br />
VALUE p, iclass;<br />
int method_changed = 0, constant_changed = 0;<br />
struct rb_id_table *const klass_m_tbl = RCLASS_M_TBL(RCLASS_ORIGIN(klass));<br />
<br />
while (module) {<br />
int superclass_seen = FALSE;<br />
struct rb_id_table *tbl;<br />
<br />
if (RCLASS_ORIGIN(module) != module)<br />
goto skip;<br />
if (klass_m_tbl && klass_m_tbl == RCLASS_M_TBL(module))<br />
return -1;<br />
/* ignore if the module included already in superclasses */<br />
for (p = RCLASS_SUPER(klass); p; p = RCLASS_SUPER(p)) {<br />
int type = BUILTIN_TYPE(p);<br />
if (type == T_ICLASS) {<br />
if (RCLASS_M_TBL(p) == RCLASS_M_TBL(module)) {<br />
if (!superclass_seen) {<br />
c = p; /* move insertion point */<br />
}<br />
goto skip;<br />
}<br />
}<br />
else if (type == T_CLASS) {<br />
if (!search_super) break;<br />
superclass_seen = TRUE;<br />
}<br />
}<br />
iclass = rb_include_class_new(module, RCLASS_SUPER(c));<br />
c = RCLASS_SET_SUPER(c, iclass);<br />
<br />
{<br />
VALUE m = module;<br />
if (BUILTIN_TYPE(m) == T_ICLASS) m = RBASIC(m)->klass;<br />
rb_module_add_to_subclasses_list(m, iclass);<br />
}<br />
<br />
if (FL_TEST(klass, RMODULE_IS_REFINEMENT)) {<br />
VALUE refined_class =<br />
rb_refinement_module_get_refined_class(klass);<br />
<br />
rb_id_table_foreach(RMODULE_M_TBL(module), add_refined_method_entry_i, (void *)refined_class);<br />
FL_SET(c, RMODULE_INCLUDED_INTO_REFINEMENT);<br />
}<br />
<br />
tbl = RMODULE_M_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) method_changed = 1;<br />
<br />
tbl = RMODULE_CONST_TBL(module);<br />
if (tbl && rb_id_table_size(tbl)) constant_changed = 1;<br />
skip:<br />
module = RCLASS_SUPER(module);<br />
}<br />
<br />
if (method_changed) rb_clear_method_cache_by_class(klass);<br />
if (constant_changed) rb_clear_constant_cache();<br />
<br />
return method_changed;<br />
}<br />
<br />
</pre><br />
<br />
== 散列表 ==<br />
<br />
== 垃圾回收 ==</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=C_%E8%AF%AD%E8%A8%80%E5%86%8D%E5%AD%A6%E4%B9%A0
C 语言再学习
2017-01-10T13:36:11Z
<p>Dennis zhuang:/* 第四节 */</p>
<hr />
<div><br />
== 从零开始的 JSON 库教程 ==<br />
<br />
原文地址: https://zhuanlan.zhihu.com/p/22457315?refer=milocode<br />
<br />
=== 第一节 ===<br />
<br />
* cmake 可以生成 xcode 项目: cmake -G Xcode<br />
* 通常枚举值用全大写(如 LEPT_NULL),而类型及函数则用小写(如 lept_type<br />
* 宏的编写技巧: 反斜线代表该行未结束,会串接下一行。而如果宏里有多过一个语句(statement),就需要用 do { /*...*/ } while(0) 包裹成单个语句。<br />
* 测试框架使用了 __LINE__ 这个编译器提供的宏,代表编译时该行的行号。如果用函数或内联函数,每次的行号便都会相同。<br />
* 关于 inline: http://blog.csdn.net/hanchaoman/article/details/7270345<br />
<br />
=== 第二节 ===<br />
<br />
* 提取重复代码,c 语言除了函数之外,宏是很重要的手段。<br />
* 用 #if 0 ... #endif 去禁用代码是一个常用技巧,而且可以把 0 改为 1 去恢复。<br />
* JSON 数字解析流程图<br />
<br />
[[文件:Json-number.png]]<br />
<br />
<br />
=== 第三节 ===<br />
<br />
* man malloc 。 realloc 可以释放传入的指针所指向的内存,并分配新的。<br />
* 我发现 c 语言提藏简洁代码。<br />
* vector 或者其他数据结构的增长因子以小于 2 为佳,比如 1.5,为什么参考 https://www.zhihu.com/question/25079705/answer/30030883<br />
* 对于很小的『函数』,直接利用宏内联是更习惯的做法,比如 lept_init<br />
* 利用 ifndef 来做默认参数配置,用户可以自定义。<br />
<br />
<pre><br />
#ifndef LEPT_PARSE_STACK_INIT_SIZE<br />
#define LEPT_PARSE_STACK_INIT_SIZE 256<br />
#endif<br />
</pre><br />
<br />
* 走到字符串到尾部,可以直接 for(;*p;p++),因为字符串末尾为 '\0',也就是 0。<br />
<br />
=== 第四节 ===<br />
<br />
* Unicode 的一些知识点:<br />
** 统一字符集(Universal Coded Character Set, UCS),每个字符映射至一个整数码点(code point),码点的范围是 0 至 0x10FFFF,码点又通常记作 U+XXXX,当中 XXXX 为 16 进位数字。例如 劲 → U+52B2、峰 → U+5CF0。很明显,UCS 中的字符无法像 ASCII 般以一个字节存储。中、日、韩的三种文字占用了Unicode中0x3000到0x9FFF的部分。<br />
** Unicode 还制定了各种储存码点的方式,这些方式称为 Unicode 转换格式(Uniform Transformation Format, UTF)。现时流行的 UTF 为 UTF-8、UTF-16 和 UTF-32。每种 UTF 会把一个码点储存为一至多个编码单元(code unit)。例如 UTF-8 的编码单元是 8 位的字节、UTF-16 为 16 位、UTF-32 为 32 位。除 UTF-32 外,UTF-8 和 UTF-16 都是可变长度编码。<br />
** U+0000 至 U+FFFF 这组 Unicode 字符称为基本多文种平面(basic multilingual plane, BMP),还有另外 16 个平面。那么 BMP 以外的字符,JSON 会使用代理对(surrogate pair)表示 \uXXXX\uYYYY。在 BMP 中,保留了 2048 个代理码点。如果第一个码点是 U+D800 至 U+DBFF,我们便知道它的代码对的高代理项(high surrogate),之后应该伴随一个 U+DC00 至 U+DFFF 的低代理项(low surrogate)。然后,我们用下列公式把代理对 (H, L) 变换成真实的码点:<br />
<pre><br />
codepoint = 0x10000 + (H − 0xD800) × 0x400 + (L − 0xDC00)<br />
</pre><br />
* UTF-8 的编码单元是 8 位字节,每个码点编码成 1 至 4 个字节。它的编码方式很简单,按照码点的范围,把码点的二进位分拆成 1 至最多 4 个字节:<br />
<br />
[[文件:utf8.png]]</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=C_%E8%AF%AD%E8%A8%80%E5%86%8D%E5%AD%A6%E4%B9%A0
C 语言再学习
2017-01-10T13:35:54Z
<p>Dennis zhuang:/* 第四节 */</p>
<hr />
<div><br />
== 从零开始的 JSON 库教程 ==<br />
<br />
原文地址: https://zhuanlan.zhihu.com/p/22457315?refer=milocode<br />
<br />
=== 第一节 ===<br />
<br />
* cmake 可以生成 xcode 项目: cmake -G Xcode<br />
* 通常枚举值用全大写(如 LEPT_NULL),而类型及函数则用小写(如 lept_type<br />
* 宏的编写技巧: 反斜线代表该行未结束,会串接下一行。而如果宏里有多过一个语句(statement),就需要用 do { /*...*/ } while(0) 包裹成单个语句。<br />
* 测试框架使用了 __LINE__ 这个编译器提供的宏,代表编译时该行的行号。如果用函数或内联函数,每次的行号便都会相同。<br />
* 关于 inline: http://blog.csdn.net/hanchaoman/article/details/7270345<br />
<br />
=== 第二节 ===<br />
<br />
* 提取重复代码,c 语言除了函数之外,宏是很重要的手段。<br />
* 用 #if 0 ... #endif 去禁用代码是一个常用技巧,而且可以把 0 改为 1 去恢复。<br />
* JSON 数字解析流程图<br />
<br />
[[文件:Json-number.png]]<br />
<br />
<br />
=== 第三节 ===<br />
<br />
* man malloc 。 realloc 可以释放传入的指针所指向的内存,并分配新的。<br />
* 我发现 c 语言提藏简洁代码。<br />
* vector 或者其他数据结构的增长因子以小于 2 为佳,比如 1.5,为什么参考 https://www.zhihu.com/question/25079705/answer/30030883<br />
* 对于很小的『函数』,直接利用宏内联是更习惯的做法,比如 lept_init<br />
* 利用 ifndef 来做默认参数配置,用户可以自定义。<br />
<br />
<pre><br />
#ifndef LEPT_PARSE_STACK_INIT_SIZE<br />
#define LEPT_PARSE_STACK_INIT_SIZE 256<br />
#endif<br />
</pre><br />
<br />
* 走到字符串到尾部,可以直接 for(;*p;p++),因为字符串末尾为 '\0',也就是 0。<br />
<br />
=== 第四节 ===<br />
<br />
* Unicode 的一些知识点:<br />
** 统一字符集(Universal Coded Character Set, UCS),每个字符映射至一个整数码点(code point),码点的范围是 0 至 0x10FFFF,码点又通常记作 U+XXXX,当中 XXXX 为 16 进位数字。例如 劲 → U+52B2、峰 → U+5CF0。很明显,UCS 中的字符无法像 ASCII 般以一个字节存储。中、日、韩的三种文字占用了Unicode中0x3000到0x9FFF的部分。<br />
** Unicode 还制定了各种储存码点的方式,这些方式称为 Unicode 转换格式(Uniform Transformation Format, UTF)。现时流行的 UTF 为 UTF-8、UTF-16 和 UTF-32。每种 UTF 会把一个码点储存为一至多个编码单元(code unit)。例如 UTF-8 的编码单元是 8 位的字节、UTF-16 为 16 位、UTF-32 为 32 位。除 UTF-32 外,UTF-8 和 UTF-16 都是可变长度编码。<br />
** U+0000 至 U+FFFF 这组 Unicode 字符称为基本多文种平面(basic multilingual plane, BMP),还有另外 16 个平面。那么 BMP 以外的字符,JSON 会使用代理对(surrogate pair)表示 \uXXXX\uYYYY。在 BMP 中,保留了 2048 个代理码点。如果第一个码点是 U+D800 至 U+DBFF,我们便知道它的代码对的高代理项(high surrogate),之后应该伴随一个 U+DC00 至 U+DFFF 的低代理项(low surrogate)。然后,我们用下列公式把代理对 (H, L) 变换成真实的码点:<br />
<pre><br />
codepoint = 0x10000 + (H − 0xD800) × 0x400 + (L − 0xDC00)<br />
</pre><br />
** UTF-8 的编码单元是 8 位字节,每个码点编码成 1 至 4 个字节。它的编码方式很简单,按照码点的范围,把码点的二进位分拆成 1 至最多 4 个字节:<br />
<br />
[[文件:utf8.png]]</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=C_%E8%AF%AD%E8%A8%80%E5%86%8D%E5%AD%A6%E4%B9%A0
C 语言再学习
2017-01-10T13:35:22Z
<p>Dennis zhuang:/* 第四节 */</p>
<hr />
<div><br />
== 从零开始的 JSON 库教程 ==<br />
<br />
原文地址: https://zhuanlan.zhihu.com/p/22457315?refer=milocode<br />
<br />
=== 第一节 ===<br />
<br />
* cmake 可以生成 xcode 项目: cmake -G Xcode<br />
* 通常枚举值用全大写(如 LEPT_NULL),而类型及函数则用小写(如 lept_type<br />
* 宏的编写技巧: 反斜线代表该行未结束,会串接下一行。而如果宏里有多过一个语句(statement),就需要用 do { /*...*/ } while(0) 包裹成单个语句。<br />
* 测试框架使用了 __LINE__ 这个编译器提供的宏,代表编译时该行的行号。如果用函数或内联函数,每次的行号便都会相同。<br />
* 关于 inline: http://blog.csdn.net/hanchaoman/article/details/7270345<br />
<br />
=== 第二节 ===<br />
<br />
* 提取重复代码,c 语言除了函数之外,宏是很重要的手段。<br />
* 用 #if 0 ... #endif 去禁用代码是一个常用技巧,而且可以把 0 改为 1 去恢复。<br />
* JSON 数字解析流程图<br />
<br />
[[文件:Json-number.png]]<br />
<br />
<br />
=== 第三节 ===<br />
<br />
* man malloc 。 realloc 可以释放传入的指针所指向的内存,并分配新的。<br />
* 我发现 c 语言提藏简洁代码。<br />
* vector 或者其他数据结构的增长因子以小于 2 为佳,比如 1.5,为什么参考 https://www.zhihu.com/question/25079705/answer/30030883<br />
* 对于很小的『函数』,直接利用宏内联是更习惯的做法,比如 lept_init<br />
* 利用 ifndef 来做默认参数配置,用户可以自定义。<br />
<br />
<pre><br />
#ifndef LEPT_PARSE_STACK_INIT_SIZE<br />
#define LEPT_PARSE_STACK_INIT_SIZE 256<br />
#endif<br />
</pre><br />
<br />
* 走到字符串到尾部,可以直接 for(;*p;p++),因为字符串末尾为 '\0',也就是 0。<br />
<br />
=== 第四节 ===<br />
<br />
* Unicode 的一些知识点:<br />
** 统一字符集(Universal Coded Character Set, UCS),每个字符映射至一个整数码点(code point),码点的范围是 0 至 0x10FFFF,码点又通常记作 U+XXXX,当中 XXXX 为 16 进位数字。例如 劲 → U+52B2、峰 → U+5CF0。很明显,UCS 中的字符无法像 ASCII 般以一个字节存储。中、日、韩的三种文字占用了Unicode中0x3000到0x9FFF的部分。<br />
** Unicode 还制定了各种储存码点的方式,这些方式称为 Unicode 转换格式(Uniform Transformation Format, UTF)。现时流行的 UTF 为 UTF-8、UTF-16 和 UTF-32。每种 UTF 会把一个码点储存为一至多个编码单元(code unit)。例如 UTF-8 的编码单元是 8 位的字节、UTF-16 为 16 位、UTF-32 为 32 位。除 UTF-32 外,UTF-8 和 UTF-16 都是可变长度编码。<br />
** U+0000 至 U+FFFF 这组 Unicode 字符称为基本多文种平面(basic multilingual plane, BMP),还有另外 16 个平面。那么 BMP 以外的字符,JSON 会使用代理对(surrogate pair)表示 \uXXXX\uYYYY。在 BMP 中,保留了 2048 个代理码点。如果第一个码点是 U+D800 至 U+DBFF,我们便知道它的代码对的高代理项(high surrogate),之后应该伴随一个 U+DC00 至 U+DFFF 的低代理项(low surrogate)。然后,我们用下列公式把代理对 (H, L) 变换成真实的码点:<br />
<pre><br />
codepoint = 0x10000 + (H − 0xD800) × 0x400 + (L − 0xDC00)<br />
</pre><br />
** UTF-8 的编码单元是 8 位字节,每个码点编码成 1 至 4 个字节。它的编码方式很简单,按照码点的范围,把码点的二进位分拆成 1 至最多 4 个字节:<br />
<br />
[[文件:utf-8.png]]</div>
Dennis zhuang
http://wiki.fnil.net/index.php?title=C_%E8%AF%AD%E8%A8%80%E5%86%8D%E5%AD%A6%E4%B9%A0
C 语言再学习
2017-01-10T13:35:07Z
<p>Dennis zhuang:/* 第四节 */</p>
<hr />
<div><br />
== 从零开始的 JSON 库教程 ==<br />
<br />
原文地址: https://zhuanlan.zhihu.com/p/22457315?refer=milocode<br />
<br />
=== 第一节 ===<br />
<br />
* cmake 可以生成 xcode 项目: cmake -G Xcode<br />
* 通常枚举值用全大写(如 LEPT_NULL),而类型及函数则用小写(如 lept_type<br />
* 宏的编写技巧: 反斜线代表该行未结束,会串接下一行。而如果宏里有多过一个语句(statement),就需要用 do { /*...*/ } while(0) 包裹成单个语句。<br />
* 测试框架使用了 __LINE__ 这个编译器提供的宏,代表编译时该行的行号。如果用函数或内联函数,每次的行号便都会相同。<br />
* 关于 inline: http://blog.csdn.net/hanchaoman/article/details/7270345<br />
<br />
=== 第二节 ===<br />
<br />
* 提取重复代码,c 语言除了函数之外,宏是很重要的手段。<br />
* 用 #if 0 ... #endif 去禁用代码是一个常用技巧,而且可以把 0 改为 1 去恢复。<br />
* JSON 数字解析流程图<br />
<br />
[[文件:Json-number.png]]<br />
<br />
<br />
=== 第三节 ===<br />
<br />
* man malloc 。 realloc 可以释放传入的指针所指向的内存,并分配新的。<br />
* 我发现 c 语言提藏简洁代码。<br />
* vector 或者其他数据结构的增长因子以小于 2 为佳,比如 1.5,为什么参考 https://www.zhihu.com/question/25079705/answer/30030883<br />
* 对于很小的『函数』,直接利用宏内联是更习惯的做法,比如 lept_init<br />
* 利用 ifndef 来做默认参数配置,用户可以自定义。<br />
<br />
<pre><br />
#ifndef LEPT_PARSE_STACK_INIT_SIZE<br />
#define LEPT_PARSE_STACK_INIT_SIZE 256<br />
#endif<br />
</pre><br />
<br />
* 走到字符串到尾部,可以直接 for(;*p;p++),因为字符串末尾为 '\0',也就是 0。<br />
<br />
=== 第四节 ===<br />
<br />
* Unicode 的一些知识点:<br />
** 统一字符集(Universal Coded Character Set, UCS),每个字符映射至一个整数码点(code point),码点的范围是 0 至 0x10FFFF,码点又通常记作 U+XXXX,当中 XXXX 为 16 进位数字。例如 劲 → U+52B2、峰 → U+5CF0。很明显,UCS 中的字符无法像 ASCII 般以一个字节存储。中、日、韩的三种文字占用了Unicode中0x3000到0x9FFF的部分。<br />
** Unicode 还制定了各种储存码点的方式,这些方式称为 Unicode 转换格式(Uniform Transformation Format, UTF)。现时流行的 UTF 为 UTF-8、UTF-16 和 UTF-32。每种 UTF 会把一个码点储存为一至多个编码单元(code unit)。例如 UTF-8 的编码单元是 8 位的字节、UTF-16 为 16 位、UTF-32 为 32 位。除 UTF-32 外,UTF-8 和 UTF-16 都是可变长度编码。<br />
** U+0000 至 U+FFFF 这组 Unicode 字符称为基本多文种平面(basic multilingual plane, BMP),还有另外 16 个平面。那么 BMP 以外的字符,JSON 会使用代理对(surrogate pair)表示 \uXXXX\uYYYY。在 BMP 中,保留了 2048 个代理码点。如果第一个码点是 U+D800 至 U+DBFF,我们便知道它的代码对的高代理项(high surrogate),之后应该伴随一个 U+DC00 至 U+DFFF 的低代理项(low surrogate)。然后,我们用下列公式把代理对 (H, L) 变换成真实的码点:<br />
<pre><br />
codepoint = 0x10000 + (H − 0xD800) × 0x400 + (L − 0xDC00)<br />
</pre><br />
** UTF-8 的编码单元是 8 位字节,每个码点编码成 1 至 4 个字节。它的编码方式很简单,按照码点的范围,把码点的二进位分拆成 1 至最多 4 个字节:<br />
[[文件:utf-8.png]]</div>
Dennis zhuang