文章目录

  • 前言
  • 1. 问题背景
  • 2. 问题复现
  • 3. Rocksdb 的 Delete-Aware 优化
    • 3.1 可配置的 Delete-Aware调度
    • 3.2 Compaction 逻辑对 delete key的优化
  • 4. Lethe: A Tunable Delete-Aware LSM Engine . SIGMOD'20

前言

本文介绍过程中涉及到的源代码是基于rocksdb 6.4.6 版本的。

同时需要对rocksdb的Compaction逻辑以及源代码有较为深入的了解,相关的原理介绍之前有一些粗浅的分析,对理解不够深入的同学应该有帮助。
1. SST文件详细格式源码解析

2. Compaction 完整实现过程 概览

1. 问题背景

基于LSM的存储引擎 在提供了优秀的写性能的基础上 却因为compaction带来了很多其他问题。

本文所要描述的一个场景是LSM引擎的标记删除(将对一个key的删除作为一个key-value 追加写入到引擎之中),这种删除逻辑会带来如下几个问题:

  • 读放大:range scan 的时候需要扫描更多的记录,点查询Bloom Filter 的false positive概率增加。
  • 空间放大:因为delete的type 也是一个key-value,而这种类型的key只有在compaction到最后一层才会被删除,中间层的很多空间就会被浪费在delete的存储之上。
  • 写放大:本身LSM compaction带来的原有问题,而针对delete key的频繁调度compaction 带来的写放大问题更不友好。
  • 用户隐私:用户已经标记删除的数据并没有及时删除,未到最后一层,无法真正删除。

如上图:标记D的都是含有delete 的sst文件,加入我们想要查找的key在L2,这个key其实之前已经被删除了。但是数据并没有被清理,对应sst文件的bloom filter也没有更新,所以无法准确判断改key是否还在,则需要一直读到 purpose key所在的sst文件的data block,发现这个key时delete type,最后才向客户端返回not found。

这个查找过程存在数次I/O操作,读sst文件的 filter block,index block, 整个data block,真删除的不存在key的查找 多了 两个block的IO流程(index block 和 data block),代价可想而知。

2. 问题复现

因为业务中存在这样的问题,所以直接上代码,更加直观。

复现如下简单且经典场景(sql删除了一个表中80%的数据,后来想scan剩下的一部分数据):

写入1千万的相同前缀 且字典序递增的key,删除其中的的9百万,从头遍历 直到取得未删除的前500条key。

站在用户的角度:

删除了一批key, 读取剩下的key中的100条

引擎的角度:

用户调用了删除接口,写入了大量的delete type的key。但是 delete type的key还在LSM 上层,一点一点做compaction到最后一层。所以用户调用一批scan 查找的过程相当于将已经标记delete 但是还未清理数据的key都得扫一遍。

通过实现一段rocksdb的复现代码如下:

#include <unistd.h>
#include <atomic>
#include <iostream>
#include <random>
#include <string>
#include <thread>#include <sys/time.h>#include "rocksdb/db.h"
#include "rocksdb/table.h"
#include "rocksdb/cache.h"
#include "rocksdb/filter_policy.h"
#include "rocksdb/write_batch.h"
#include "rocksdb/rate_limiter.h"
#include "rocksdb/perf_context.h"
#include "rocksdb/iostats_context.h"
#include "rocksdb/table_properties.h"
#include "utilities/table_properties_collectors.h"
#include "gflags/gflags.h"#define VALUE_LEN 500 using namespace google;DEFINE_int64(time,1200,"read threads' ratio");
DEFINE_int64(thread_num,64,"total threads ");
DEFINE_string(db_dir, "srd_delete", "db's dir is delete");
DEFINE_bool(use_fullcompaction, false, "use full compaction to db");
DEFINE_int64(num_keys, 1000000,"write keys' num");
DEFINE_int64(wait_compaction,10,"wait");
DEFINE_bool(traverse, true, "use traverse");
DEFINE_bool(use_delete_collector, false, "use traverse");std::atomic<long> g_op_W;rocksdb::DB* src_db;
rocksdb::Options options;
std::mt19937_64 generator_; // 生成伪随机数// 返回微秒
inline int64_t now() {struct timeval t;gettimeofday(&t, NULL);return static_cast<int64_t>(t.tv_sec)*1000000 + t.tv_usec;
}// 打开rocksdb
void OpenDB() {options.create_if_missing = true;options.compression = rocksdb::kNoCompression;options.disable_auto_compactions = FLAGS_use_fullcompaction;if(FLAGS_use_delete_collector) {options.table_properties_collector_factories.emplace_back(rocksdb::NewCompactOnDeletionCollectorFactory(1000000, 1000));}std::shared_ptr<rocksdb::Cache> cache = rocksdb::NewLRUCache(10737418240);rocksdb::BlockBasedTableOptions bbto;bbto.whole_key_filtering = true;bbto.cache_index_and_filter_blocks = true;bbto.filter_policy.reset(rocksdb::NewBloomFilterPolicy(16,false));bbto.block_cache = cache;options.table_factory.reset(rocksdb::NewBlockBasedTableFactory(bbto));options.max_background_compactions = 32;auto s = rocksdb::DB::Open(options, FLAGS_db_dir, &src_db);std::cout << "open src_db" <<  s.ToString() << std::endl;}// 配置手动执行full compaction
void FullCompaction() {rocksdb::Slice begin("");rocksdb::Iterator *it = src_db->NewIterator(rocksdb::ReadOptions());it->SeekToLast();std::string end_str = it->key().ToString();delete it;rocksdb::Slice end(end_str);auto s = src_db->CompactRange(rocksdb::CompactRangeOptions(), &begin, &end);if (!s.ok()) {std::cout << "CompactRange failed " << s.ToString() << std::endl;}
}// 写入 + 删除
void DOWrite(int num) {int count = FLAGS_num_keys;std::string value(VALUE_LEN, 'x'); while(count > 0) {src_db -> Put(rocksdb::WriteOptions(), std::string("test_compaction_129_")+std::to_string(count),value);++ g_op_W;count --;}// 删除90% 的keystd::cout << "Begin delete "<< std::endl;for (int i = 0;i < FLAGS_num_keys - FLAGS_num_keys / 10; ++i) {src_db->Delete(rocksdb::WriteOptions(), std::string("test_compaction_129_")+std::to_string(i));}src_db->SetOptions(src_db->DefaultColumnFamily(),{{"diable_auto_compactions","true"}});std::cout << "End delete \n";// 开启full compactionif(FLAGS_use_fullcompaction) {FullCompaction();sleep(FLAGS_wait_compaction);}
}void Traverse() {// open perf rocksdb::SetPerfLevel(rocksdb::PerfLevel::kEnableTimeExceptForMutex);rocksdb::get_perf_context()->Reset();rocksdb::get_iostats_context()->Reset();rocksdb::Iterator *it = src_db->NewIterator(rocksdb::ReadOptions());it->Seek("");std::cout << "Traverse begin :" << std::endl;int64_t ts = now();int seek_count = 0;for(; it->Valid()&& seek_count<500;  it->Next() ) {std::cout << it->key().ToString() << std::endl; seek_count++;}delete it;// close perf,查看internal_delete_skipped_count指标rocksdb::SetPerfLevel(rocksdb::PerfLevel::kDisable);std::cout << "Traverse use time :" << now() - ts << std::endl;std::cout << "internal_delete_skipped_count "  << rocksdb::get_perf_context()->internal_delete_skipped_count<< std::endl;
}int main(int argc, char** argv) {ParseCommandLineFlags(&argc,&argv, true);OpenDB();for(int i = 0;i < FLAGS_thread_num; i++) {new std::thread(DOWrite,i);}long last_opn_W = 0;int count = FLAGS_time;while(count > 0) {sleep(1);long nopn_W = g_op_W;std::cout << "write_speed : " << nopn_W - last_opn_W << std::endl;last_opn_W = nopn_W;count --;}if(FLAGS_traverse){Traverse();}return 0;
}

编译时需要加入gflags 的链接,最终可以通过如下命令进行测试:

测试原生compaction逻辑下 traverse过程的耗时

./test_delete -use_fullcompaction=false -time=300 -db_dir=./src_delete -thread_num=1 -num_keys=10000000 -use_delete_collector=false

可以发现最终的输出:

Traverse use time :2018292

也就是在客户端看来,只获取100个存在的key就话费2s的时间。。。这简直不能忍,这样的存储引擎如何让用户安心使用。

这里尝试使用 Full Compaction 进行优化,即rocksdb自己提供的手动compaction接口CompacRange,在删除操作执行完成之后对整个db进行了一次全量的compaction操作,再次进行遍历获取100个存在key的耗时统计。

以上的代码也有增加,使用如下命令运行

./test_delete -use_fullcompaction=true -time=300 -db_dir=./src_delete -thread_num=1 -num_keys=10000000 -use_delete_collector=false

可以看到这次的运行耗时下降了3个量级,仅仅只有364us:

Traverse use time :364

那是不是我们这个问题就解决了呢?显然不可能。

全量compaction 带来的代价是极大的,所有的sst文件不论之前有没有参与过compaction都会被调度起来,巨量的I/O 和 CPU 洪峰,且跟随本节点数据量的大小持续一段时间;这个过程可能直接让系统的可用性极低甚至掉零,更为严重的是某位不讲武德的仁兄看到优化效果如此给力,不经过严格的测试直接线上跑 ,那这位仁兄就可以准备好挂个p0 走人了。

况且存储引擎作为持久化存储的核心,这样的危险操作显然不会交给用户来做,所以本身引擎内部要有足量机制降低 未compaction清理的delete key对scan性能的影响,这也是引擎的职责所在。

总结这个问题放在引擎中处理 的几个关节点 如下:

  • delete的过程是通过compaction进行的,而compaction对于用户来说是黑盒,用户很难精确控制compaction过程中的delete key清理时机。
  • 实际LSM的应用场景中 用户驱动删除的workload 相对较少,除了用户主动删除之外LSM 引擎也会应用在关系数据库之中 出现drop 表等被动删除场景,所以在用户不知道的情况下引擎也会产生大范围的数据清理和迁移。
  • 对非连续的key的删除调度。比如key的相同前缀是基于table id,删除时则基于某一时间戳进行删除。这个过程需要调度seek进行大量的查找,代价极大。

接下来将介绍一下 rocksdb 以及 业界对该场景的优化手段。rocksdb这里比较熟悉,会深入源代码详细介绍一番;业界对该优化的过程将介绍几篇已有的优化论文。

3. Rocksdb 的 Delete-Aware 优化

基于以上问题,接下来会介绍Rocksdb 在该场景所做的两个优化feature。

Rocksdb 在保证不增加LSM 的读代价的情况下 针对delete type的key增加了两方面的优化逻辑:

  • 用户态可配置的delete 密集型 sst文件的compaction优先级调度。
  • compaction本身针对 delete key 逻辑的 处理优化。

3.1 可配置的 Delete-Aware调度

OpenDB函数中增加如下rocksdb配置:

options.table_properties_collector_factories.emplace_back(rocksdb::NewCompactOnDeletionCollectorFactory(10000, 1000));

同时需要包含头文件:

#include "rocksdb/table_properties.h"
#include "utilities/table_properties_collectors.h"

查看以上函数声明:

std::shared_ptr<CompactOnDeletionCollectorFactory>NewCompactOnDeletionCollectorFactory(size_t sliding_window_size,size_t deletion_trigger) {return std::shared_ptr<CompactOnDeletionCollectorFactory>(new CompactOnDeletionCollectorFactory(sliding_window_size, deletion_trigger));
}// A factory of a table property collector that marks a SST
// file as need-compaction when it observe at least "D" deletion
// entries in any "N" consecutive entires.
//
// @param sliding_window_size "N"
// @param deletion_trigger "D"
CompactOnDeletionCollectorFactory(size_t sliding_window_size,size_t deletion_trigger): sliding_window_size_(sliding_window_size),
deletion_trigger_(deletion_trigger) {}

总结一下:

NewCompactOnDeletionCollectorFactory函数声明一个用户配置的表格属性收集器,这里的表格指的是sstable。

该函数需要传入的两个参数sliding_window_sizedeletion_trigger 表示删除收集器 的滑动窗口 和 触发删除的key的个数。

在每个sst文件内维护一个滑动窗口,滑动窗口维护了一个窗口大小,在当前窗口大小内出现的delete-key的个数超过了窗口的大小,那么这个sst文件会被标记为need_compaction_,从而在下一次的compaction过程中被优先调度。

达到将delete-key较多的sst文件快速下沉到底层,delete的数据被真正删除的目的,可以理解为这是一种更加激进的compaction调度策略。

使用者仅仅需要调整两个参数的比例,即可决定该sst文件是否应该被优先调度compaction。

接下来详细说一下Rocksdb的这个配置如何实现deletes 超过一定比例之后被compaction优先调度:

用户配置了table_properties_collector_factories

在Compaction的核心处理key-value的函数CompactionJob::ProcessKeyValueCompaction 中调度 BlockBasedTableBuilder::Add 函数 构造sst文件中不同存储区域的builder,这里面会使用用户构造的collector 进行 指定key的收集,通过如下函数NotifyCollectTableCollectorsOnAdd 进入 InternalAdd

bool NotifyCollectTableCollectorsOnAdd(const Slice& key, const Slice& value, uint64_t file_size,const std::vector<std::unique_ptr<IntTblPropCollector>>& collectors,Logger* info_log) {bool all_succeeded = true;for (auto& collector : collectors) {Status s = collector->InternalAdd(key, value, file_size); all_succeeded = all_succeeded && s.ok();if (!s.ok()) {LogPropertiesCollectionError(info_log, "Add" /* method */,collector->Name());}}return all_succeeded;
}

因我我们reset的是NewCompactOnDeletionCollectorFactory collector,所以会通过UserKeyTablePropertiesCollector::InternalAdd 进入CompactOnDeletionCollector::AddUserKey collector的AddUserKey函数:

  • 增加滑动窗口内 真正key的数量,并根据是否达到了窗口阈值 来及时更新内部key的数量 以及 降低旧的delete key的数量
  • 根据传入的key的类型来 增加滑动窗口内 delete key的数量,达到删除阈值时会打入need_compaction_标记(这个标记会在后续的compaction逻辑中将当前sst文件标记为优先级较高的sst文件)
Status CompactOnDeletionCollector::AddUserKey(const Slice& /*key*/,const Slice& /*value*/,EntryType type,SequenceNumber /*seq*/,uint64_t /*file_size*/) {...// 这里的bucket_size_可以理解为我们设置的滑动窗口大小// 第一个if语句中做的事情主要是更新滑动窗口中 key的数量 以及 旧的delete key的数量if (num_keys_in_current_bucket_ == bucket_size_) {// When the current bucket is full, advance the cursor of the// ring buffer to the next bucket.current_bucket_ = (current_bucket_ + 1) % kNumBuckets;// Update the current count of observed deletion keys by excluding// the number of deletion keys in the oldest bucket in the// observation window.assert(num_deletions_in_observation_window_ >=num_deletions_in_buckets_[current_bucket_]);num_deletions_in_observation_window_ -=num_deletions_in_buckets_[current_bucket_];num_deletions_in_buckets_[current_bucket_] = 0;num_keys_in_current_bucket_ = 0;}num_keys_in_current_bucket_++;// 根据key的类型来调整是否达到触发删除的阈值,从而打入need_compaction_标记if (type == kEntryDelete) { num_deletions_in_observation_window_++;num_deletions_in_buckets_[current_bucket_]++;if (num_deletions_in_observation_window_ >= deletion_trigger_) {need_compaction_ = true;}}...
}

在collector中达到触发删除的标记之后会在BlockBasedTableBuilder::NeedCompact()中体现,而这个函数则会被CompactionJob::FinishCompactionOutputFile调用

bool BlockBasedTableBuilder::NeedCompact() const {for (const auto& collector : rep_->table_properties_collectors) {if (collector->NeedCompact()) {return true;}}return false;
}

CompactionJob::FinishCompactionOutputFile同样是在compaction的核心逻辑ProcessKeyValueCompaciton 中调用,以sst文件为单位 将构造好的一个个builder写入到具体的sst文件中的数据区域,写入过程中对要写入的sst文件打入标记(如果collector中确认delete keys超过了设置的阈值)

 meta->marked_for_compaction = sub_compact->builder->NeedCompact();

marked_for_compaction标记会在函数VersionStorageInfo::ComputeFilesMarkedForCompaction 被使用,其中将有marked_for_compaction标记文件添加到一个动态数组之中files_marked_for_compaction_,这个数组会在当前compaction完成之后再次被调度,将动态数组中的sst文件优先进行compaction的调度。

void VersionStorageInfo::ComputeFilesMarkedForCompaction() {files_marked_for_compaction_.clear();int last_qualify_level = 0;// Do not include files from the last level with data// If table properties collector suggests a file on the last level,// we should not move it to a new level.for (int level = num_levels() - 1; level >= 1; level--) {if (!files_[level].empty()) {last_qualify_level = level - 1;break;}}for (int level = 0; level <= last_qualify_level; level++) {for (auto* f : files_[level]) {if (!f->being_compacted && f->marked_for_compaction) {// 将被打入标记的文件添加到动态数组之中files_marked_for_compaction_.emplace_back(level, f); }}}
}

后续的调度过程会在DBImpl::BackgroundCompaction中,通过如下逻辑调用 LevelCompactionPickerNeedsCompaction来进行判断,判断的过程主要是确认files_marked_for_compaction_不为空即返回true,从而再次将文件添加到当前column family的待compaction队列,最后通过MaybeScheduleFlushOrCompaction再次调度。

          if (cfd->NeedsCompaction()) {// Yes, we need more compactions!AddToCompactionQueue(cfd);++unscheduled_compactions_;MaybeScheduleFlushOrCompaction();}

整个链路相对来说比较长且复杂的,需要对Compaction的调度代码较为熟悉的话会更容易理解。

最后能够在rocksdb的LOG日志来确认是否触发了Marked的逻辑:

"compaction_reason": "ManualCompaction", 这样的字段,且后续的Compaction job的输出中能够看到records in的数据 和records dropped的数据的比例满足我们设置的collector中的 sliding-window-size 和 delete-trigger的比例即可。

最后的性能可以通过上一章节中的 问题复现 的代码看到。
运行如下命令:

./test_delete -use_fullcompaction=false -time=300 -db_dir=./src_delete -thread_num=1 -num_keys=10000000 -use_delete_collector=true

可以看到相同的delete场景,最后的traverse性能相比于原来能够提升4倍,这个数据在不同的delete 比重以及collector设置的参数下有差异的。

Traverse use time :674629

3.2 Compaction 逻辑对 delete key的优化

Compaction调度过程不再深入描述了,直接到上小节说过的ProccessKeyValueCompaction函数中会对key-value一个一个处理,会通过Compaction迭代器的移动(基本的SeekToFirst,Next等)来达到一个一个处理的目的。

处理的过程通过调用NextFromInput函数来进行,这个函数会处理不同的key-type,其中关于delete类型的主要处理逻辑如下代码(这一部分全部粘贴到下面了,可以简单看看):

我们知道原本LSM 中的delete key 只能在最后一层将之前所有的写入包括自己都清理掉,因为到了最后一层就知道之前已经没有人再使用这个key了。

这里可能有人会问,用户已经删除了,为什么还一定要到最后一层才将key的数据清理掉呢?

因为rocksdb提供了snapshot功能,即在当前delete之前可能存在用户打入的snapshot,即rocksdb需要保证那个时刻的key能够被读到,所以删除的过程中都需要确认当前key之前版本是否有snapshot的sequence number,如果没有才允许后续的删除判断。

} else if (compaction_ != nullptr && ikey_.type == kTypeDeletion &&IN_EARLIEST_SNAPSHOT(ikey_.sequence) &&ikeyNotNeededForIncrementalSnapshot() &&compaction_->KeyNotExistsBeyondOutputLevel(ikey_.user_key,&level_ptrs_)) {// TODO(noetzli): This is the only place where we use compaction_// (besides the constructor). We should probably get rid of this// dependency and find a way to do similar filtering during flushes.//// For this user key:// (1) there is no data in higher levels// (2) data in lower levels will have larger sequence numbers// (3) data in layers that are being compacted here and have//     smaller sequence numbers will be dropped in the next//     few iterations of this loop (by rule (A) above).// Therefore this deletion marker is obsolete and can be dropped.//// Note:  Dropping this Delete will not affect TransactionDB// write-conflict checking since it is earlier than any snapshot.//// It seems that we can also drop deletion later than earliest snapshot// given that:// (1) The deletion is earlier than earliest_write_conflict_snapshot, and// (2) No value exist earlier than the deletion.++iter_stats_.num_record_drop_obsolete;if (!bottommost_level_) {++iter_stats_.num_optimized_del_drop_obsolete;}input_->Next();} else if ((ikey_.type == kTypeDeletion) && bottommost_level_ &&ikeyNotNeededForIncrementalSnapshot()) {// Handle the case where we have a delete key at the bottom most level// We can skip outputting the key iff there are no subsequent puts for this// keyParsedInternalKey next_ikey;input_->Next();// Skip over all versions of this key that happen to occur in the same snapshot// range as the deletewhile (input_->Valid() && ParseInternalKey(input_->key(), &next_ikey) &&cmp_->Equal(ikey_.user_key, next_ikey.user_key) &&(prev_snapshot == 0 ||DEFINITELY_NOT_IN_SNAPSHOT(next_ikey.sequence, prev_snapshot))) {input_->Next();}// If you find you still need to output a row with this key, we need to output the// delete tooif (input_->Valid() && ParseInternalKey(input_->key(), &next_ikey) &&cmp_->Equal(ikey_.user_key, next_ikey.user_key)) {valid_ = true;at_next_ = true;}}

以上逻辑的优化主要体现在第一个if语句之中,即当前delete 的key可能不在最后一层,通过这个函数KeyNotExistsBeyondOutputLevel判断后续的层中没有当前key了, 那么就可以直接删除;这个逻辑会增加一定的CPU比较代价,但是它的收益是可观的,能够在较高的层中将key直接删除。

4. Lethe: A Tunable Delete-Aware LSM Engine . SIGMOD’20

这是一篇 今年顶会 SIGMOD中发表的论文,核心就是LSM 引擎带来的delete问题。

在该篇论文中详细描述了delete 对LSM带来的问题,且现有的引擎没有很好的办法很好得解决大范围删除的问题。

Lethe通过:

  • 增加统计信息和compaction策略,优化sort key大范围删除带来的问题
  • 设计新的storage layout,来提升secondary key的大范围删除。

提出了了两种策略:

  • FADE(fast deletion): 更加激进的compaction调度策略。相比于rocksdb的可配置策略,这里增加了文件的TTL,优先针对达到TTL的文件进行挑选(可以根据TTL主动触发调度),调度compaction删除。

  • KiWi(Key Weaving storage layout): 提高支持非sort key的range delete,牺牲了一定的查询代价,提升了范围删除的效率。这个设计 感觉和rocksdb的DeleteRange有点类似,就是不知道性能差异大吗?

一个X-engine的大佬已经有一篇该论文的详细分享了。

Lethe 如何优化 LSM-Tree delete 难题

论文原地址: https://cs-people.bu.edu/dstara/pdfs/Lethe.pdf

后续尝试将Lethe这种更加激进的策略集成到rocksdb中,看看效果如何,理论上在Delete 比例重的场景,应该优于Rocksdb本身的优化。

LSM 优化系列(四) -- Rocksdb和Lethe 对Delete问题的优化相关推荐

  1. JVM性能优化(四)提高网站访问性能之Tomcat优化

    一.前言 tomcat 服务器在JavaEE项目中使用率非常高,所以在生产环境对tomcat的优化也变得非常重要了,对于tomcat的优化,主要是从2个方面入手,一是tomcat本身的配置,另一个是t ...

  2. JVM性能优化(四)提高网站访问性能之Tomcat优化,晒出收入

    vim tomcat-users.xml 需要配置文件,配置tomcat的管理用户 写入以下内容: 保存退出 如果是tomcat7,配置了tomcat用户就可以登录系统了,但是tomcat8中不行,还 ...

  3. JVM 性能优化(四)提高网站访问性能之 Tomcat 优化,java 程序开发实用教程邱加永答案

    启动 tomcat 2.33 启动访问 成功访问 tomcat 地址后,点击 首页中Server Status,输入用户名密码tomcat/tomcat 进入页面,我们需要关注的就是其中 JVM 的列 ...

  4. SSE图像算法优化系列二十二:优化龚元浩博士的曲率滤波算法,达到约1000 MPixels/Sec的单次迭代速度...

      2015年龚博士的曲率滤波算法刚出来的时候,在图像处理界也曾引起不小的轰动,特别是其所说的算法的简洁性,以及算法的效果.执行效率等方面较其他算法均有一定的优势,我在该算法刚出来时也曾经有关注,不过 ...

  5. 「性能优化系列」APP内存优化理论与实践

    当一个应用同时运行越来越多的任务以及复杂的业务,Android系统的内存管理机制已经无法满足内存的释放与回收,为了应用的稳定性与性能,去控制内存的创建和回收就成为了一个重要的命题. 本篇文章主要涉及内 ...

  6. 游戏优化系列三:Unity游戏的黑屏问题解决方法

    作者 大家好,我叫Jack冯: 本人20年硕士毕业于广东工业大学,于2020年6月加入37手游安卓团队:目前主要负责海外游戏发行安卓相关开发. 系列目录 游戏优化系列一:海外谷歌应用适配相关 游戏优化 ...

  7. LSM优化系列(五) -- 【SIGMOD‘19】X-engine 在电商场景下针对大规模事务处理的优化-- 强者恒强啊

    文章目录 1. 前言 2. 论文结构 2.1 海啸 问题 2.2 泄洪 问题 2.3 洋流 问题 3. X-engine架构 3.1 读路径优化 概览 3.2 写路径优化概览 3.3 Flush和Co ...

  8. CocosCreator客户端优化系列(四):CPU占用及性能优化

    来自:https://blog.csdn.net/zzx023/article/details/88991314 CocosCreator客户端优化系列(四):CPU占用及性能优化 这篇文章是优化系列 ...

  9. SSE图像算法优化系列十四:局部均方差及局部平方差算法的优化。

    关于局部均方差有着较为广泛的应用,在我博客的基于局部均方差相关信息的图像去噪及其在实时磨皮美容算法中的应用及使用局部标准差实现图像的局部对比度增强算法中都有谈及,即可以用于去噪也可以用来增强图像,但是 ...

最新文章

  1. 2016.8.11 DataTable合并及排除重复方法
  2. 深度学习3:手动实现L2正则化(L2 Regularization)
  3. 揭秘:GitHub Star 5W人追更,这个框架是打工人石锤了!
  4. elk6.3.1版本+metricbeat监控收集swarm的资源使用情况
  5. html中<pre>标签
  6. 【Paper】An Experiment Comparing Double Exponential Smoothing and Kalman Filter-Based Predict
  7. Java快速生成20亿数字_关于内存:Java-打印10亿到20亿
  8. NDK 获取android的imei和serial number
  9. Spring 使用Cache(转)
  10. C#制作Windows service服务系列二:演示一个定期执行的windows服务及调试(windows service)(转载)...
  11. 小程序短视频项目———上传短视频业务流程简介
  12. JAVA实现求五个数阶乘之和 小实例
  13. 一天快速入门 Python
  14. ios 倒数器_如何使用倒数计时器来停止游戏 – iOS [SWIFT] –
  15. 写给新入职的毕业生们
  16. 【论文解读】MacBERT: 中文自然语言预训练模型
  17. netdev_features_t和ip_summed说明
  18. 从开发转到安全渗透工程师,是我做的最对的决定
  19. HTML中引入外部CSS和JS
  20. 单片机硬件按电路设计实例

热门文章

  1. es6学习笔记8--Map数据结构
  2. [转]优化Flash性能
  3. 中国对计算机科学与技术人才的需求,计算机科学与技术整体概况之人才需求分析_跨考网...
  4. 如何设置PHP常量,我应该如何保持我的常量在PHP
  5. 搜狗手机输入法php,在线调用搜狗云输入法
  6. php mysql 防 sql注入_php 防sql注入方法
  7. python修改文件内容_Python批量修改文本文件内容的方法详解
  8. java设计模式中不属于创建型模式_23种设计模式第二篇:java工厂模式定义:工厂模式是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式...
  9. 滑坡的剖面图怎么用计算机绘制,cad怎么画滑坡剖面
  10. android studio islibrary,通过AndroidStudio发布Android Library到Jcenter[超详细]