原文地址:https://blog.fanscore.cn/p/22/

一、背景

公司当前有一个用户群的系统,核心功能是根据不同的条件组去不同的业务线中get符合条件的uid列表,然后存到redis中的bitmap中。

举个,如果一个用户群中有两个用户: 3和7,即[3,7],用bitmap表示那就是:00010001

最后利用redis提供的bitOp命令: bitOp AND \ bitOp XOR \ bitOp OR对各个条件组对应的uid列表bitmap做交并差集计算,得出最终的用户群并存储到redis bitmap中。

二、问题

对于上面描述的系统,如果用户群人数较多的那我们就需要执行较多次的setBit {uid} 1命令,而且如果用户群中的第一个uid是一个特别大的值比如10亿的话,就可能会一次malloc 1000000000/1024/1024/8 ~= 120M的内存,这可能会导致redis卡住一段时间,在高并发的redis实例上执行这个操作是相当危险的。而且可以预想到对于两个较大的bitmap key执行bitOp也是非常消耗CPU的,应该尽量避免在存储型的redis实例中做这种十分消耗CPU的计算操作。

三、解决方案

针对上述的问题,可以将bitmap的计算挪到应用程序中来,只将最终统计出来的bitmap存储到redis中即可。

  如果最终结果用户群中的第一个uid是一个特别大的值的话,可以先set 1K再设置2K..3K...这样缓存的增加bitmap的大小避免redis卡住。

四、PHP实现Bitmap

由于该系统目前是使用的PHP,所以下面记录下PHP实现Bitmap的”心路历程“。

由于要操作PHP变量的某一位,所以就要借助位运算来实现,但是又由于PHP的位运算只能作用在整型数上,所以我们无法使用字符串或者浮点数来实现,所以最先考虑的就是使用整型数组来实现。

为什么是数组呢?因为在64位机器上一个整型变量最多只能使用64位,又由于PHP的整型是有符号的,所以最高位无法供我们使用,所以一个整型变量能存储的最大的uid就是63,这真是太鸡肋了-_-||,所以只能搞个用多个整型变量了实现了。

OK,到此为止貌似找到一个看起来不错的解决方案。但是我们再思考这样一个问题:假设我们系统中最大的uid是63x100万=3.6千万(对主流互联网公司来说这很正常吧),那为了存储所有uid,我们需要1百万个整数才行,即我们需要一个拥有1百万个元素的数组,那么如果我在进程中制造了一个这样的数组会占用多少内存呢?会是64 * 1百万 / 1024 / 1024 / 8 ~= 7.6M吗?答案是否定的,因为php数组是由HashTable实现的,这是一个复杂的结构体,除了数组元素占用的内存外,还有其他的占用。(这里先不做展开,有兴趣可以自行查看下php数组的实现)

眼见为实:

<?php
ini_set('memory_limit','4G');
$arr = [];
for ($i = 0; $i < 64 * 1000000; $i++)
{
$arr[] = PHP_INT_MAX;
} echo "done\n";
while(1){
}

查看内存占用

可以看到大概是1.5G,比我们上面预计的大的多,这太可怕了,必须优化下我们的内存占用,才能真正在生产环境中使用。

这里需要提一句,我的机器只有8G,所以程序可能会用到swap分区,而ps命令结果中的RSS不统计swap分区的占用,在我实际实现中发现ps结果中RSS一列显示占用的内存会随着时间慢慢减少,但是我的程序中arr变量占用的内存是不可能被回收的,所以推测是物理内存中占用的部分内存被置换到了swap分区中。如果你要进行这个实验的话建议关闭swap分区,这样你能得到一个更准确的结果。

五、继续优化

基于上面的经验,如果我们要占用尽可能小的内存,那我们必须能够操作一段近乎无限长的内存且不能产生其他额外占用才可以。幸运的是PHP给我们提供了这样一个扩展:GMP,这个扩展可以让我们使用一个任意长度的整数。OK现在我们拥有了获得一块连续的内存而不会产生其他额外占用的手段,再写一段代码使用下并验证下内存占用情况:

<?php

$gmp = gmp_init(0);
gmp_setbit($gmp, 64 * 1000000, true);
echo "done\n";
while(1){}

Awesome,这次只使用了15M的内存。更加兴奋的是这个扩展提供了诸如:gmp_andgmp_orgmp_xor这样进行位运算的函数,极大的方便了我们的使用。

到此为止我们似乎找到了一个完美的解决方案,但是真的完美吗?No!其实还可以再优化一下,想象下如果我们有一个用户群,里面只有一个uid:64000000(表示为数组的话就是:[64000000]),为了存储这个用户我们需要占用7.6M内存,而这个用户群中仅仅只有一个元素,这真是极大的浪费啊!

为了优化这个问题可以拥抱上面被我们唾弃的数组,一个大的bitmap拆分为一个个小bitmap的数组,这一个个小的bitmap我们限制大小为1Kw位。

回到上面的问题,如果我们要存储[64000000]这个用户群的话只需要在数组的第6个元素中设置一个little bitmap: 1即可。这样我们就由一开始的占用7.6M内存优化为了占用1位内存。

OK,到此为止我们找到一个还不错的解决方案。

后言

为了在Mac中安装GMP扩展又耗费了很多时间,当然,这又是另外一个故事了。有时间我会分享Mac中安装GMP扩展的过程中我遇到的问题。

参考资料

  1. GNU Multiple Precision
  2. Process Memory Management in Linux
  3. 从源码看 PHP 7 数组的实现

PHP实现Bitmap的探索 - GMP扩展使用的更多相关文章

  1. PHP中操作任意精度大小的GMP扩展学习

    对于各类开发语言来说,整数都有一个最大的位数,如果超过位数就无法显示或者操作了.其实,这也是一种精度越界之后产生的精度丢失问题.在我们的 PHP 代码中,最大的整数非常大,我们可以通过 PHP_INT ...

  2. CDHtmlDialog探索----WebBrowser扩展和网页Javascript错误处理

    当WebBrowser控件(CDHtmlDialog自动创建了WebBrowser控件)加载的网页中含有错误Javascript代码时默认情况下控件会弹出错误信息提示对话框,相对于用户体验来说这样的提 ...

  3. PHP内核探索之变量(6)- 后续内核探索系列大纲备忘

    年前因为工作比较饱和,现在又忙着换工作的事情,基本停止了对博文的更新.后续的博文,还是慢慢补上吧. 为了不至于过于发散,先搞个未成形的大纲,如下: PHP内核探索之变量  不平凡的字符串 PHP内核探 ...

  4. 使用响应扩展的响应面(Rx)

    下载demo - 196 KB 下载source - 98 KB 表的内容 系统要求反应面一个简单的计时器从事件中收集数据序列使用更复杂的查询订阅您希望完成的面最终考虑历史 介绍 "Rx&q ...

  5. Docker PHP 扩展配置

    # PHP 容器配置 # 从官方基础版本构建 FROM php:7.2-fpm # 官方版本默认安装扩展: # Core, ctype, curl # date, dom # fileinfo, fi ...

  6. Android - 利用扩展函数为Bitmap添加文字水印

    <异空间>项目技术分享系列--扩展函数为Bitmap添加文字水印 对图片Bitmap绘制文字水印还是比较常见的需求,毕竟版权意识都在增强(用户可以给自己图片加上用户名),还可以为用户提供更 ...

  7. Sass介绍及入门教程

    Sass是什么? Sass是"Syntactically Awesome StyleSheets"的简称.那么他是什么?其实没有必要太过于纠结,只要知道他是“CSS预处理器”中的一 ...

  8. 论文翻译:BinaryNet: Training Deep Neural Networks with Weights and Activations Constrained to +1 or −1

    目录 摘要 引言 1.BinaryNet 符号函数 梯度计算和累积 通过离散化传播梯度 一些有用的成分 算法1 使用BinaryNet训练DNN 算法2 批量标准化转换(Ioffe和Szegedy,2 ...

  9. ICML 2018 | 从强化学习到生成模型:40篇值得一读的论文

    https://blog.csdn.net/y80gDg1/article/details/81463731 感谢阅读腾讯AI Lab微信号第34篇文章.当地时间 7 月 10-15 日,第 35 届 ...

随机推荐

  1. java中对 闰年的计算 以及月份天数

    import java.io.*;//局部变量的使用import java.util.Scanner; public class HelloJava {     public static void ...

  2. Istio 运维实战系列(3):让人头大的『无头服务』-下

    本系列文章将介绍用户从 Spring Cloud,Dubbo 等传统微服务框架迁移到 Istio 服务网格时的一些经验,以及在使用 Istio 过程中可能遇到的一些常见问题的解决方法. 失败的 Eur ...

  3. Centos-清屏命令-clear

    clear 清理屏幕输出 相关快捷键 ctrl + l

  4. python使用xpath(超详细)

    使用时先安装 lxml 包 开始使用 和beautifulsoup类似,首先我们需要得到一个文档树 把文本转换成一个文档树对象 from lxml import etree if __name__ = ...

  5. Windows 无法验证此设备所需的驱动程序的数字签名”的问题

    转载: 1.https://jingyan.baidu.com/article/375c8e19c2b25b25f2a229a3.html 2. https://jingyan.baidu.com/a ...

  6. P5322 排兵布阵解题报告

    本想在洛谷上交篇题解的,结果发现交不了,所以只能在这边写了... 作为一个蒟蒻,看到省选题,第一眼考虑怎么打暴力 我们可以分情况考虑 当\(s==1\)的时候 我们可以把他当成一个\(01\)背包,背 ...

  7. Python中字符串有哪些常用操作?纯干货超详细

  8. 把python文件打包成可执行文件(win10实验成功)

    总是有人来找我帮看下工单状态,又懒得写页面展示出来,干脆打包成exe文件好啦 打包很简单,难点在于安装pyinstaller这个依赖包,主要是网络问题~ 我也是参考别人的博文,别人的文章写得很详细,我 ...

  9. regsvr32 bypass windows defender 新思路

    原文链接:blog 在对regsvr32的用法进行了解之后,对于Casey Smith的远程js脚本执行命令的思路很感兴趣. 命令语法如下: regsvr32 /s /n /u /i:http://1 ...

  10. 多测师讲解python_003.2练习题

    # 1.分别打印100以内的所有偶数和奇数并存入不同的列表当中# 2.请写一段Python代码实现删除一个list = [1, 3, 6, 9, 1, 8]# 里面的重复元素不能用set# 3.将字符 ...