PHP实现Bitmap的探索 - GMP扩展使用
原文地址: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_and、gmp_or、gmp_xor这样进行位运算的函数,极大的方便了我们的使用。
到此为止我们似乎找到了一个完美的解决方案,但是真的完美吗?No!其实还可以再优化一下,想象下如果我们有一个用户群,里面只有一个uid:64000000(表示为数组的话就是:[64000000]),为了存储这个用户我们需要占用7.6M内存,而这个用户群中仅仅只有一个元素,这真是极大的浪费啊!
为了优化这个问题可以拥抱上面被我们唾弃的数组,一个大的bitmap拆分为一个个小bitmap的数组,这一个个小的bitmap我们限制大小为1Kw位。

回到上面的问题,如果我们要存储[64000000]这个用户群的话只需要在数组的第6个元素中设置一个little bitmap: 1即可。这样我们就由一开始的占用7.6M内存优化为了占用1位内存。
OK,到此为止我们找到一个还不错的解决方案。
后言
为了在Mac中安装GMP扩展又耗费了很多时间,当然,这又是另外一个故事了。有时间我会分享Mac中安装GMP扩展的过程中我遇到的问题。
参考资料
PHP实现Bitmap的探索 - GMP扩展使用的更多相关文章
- PHP中操作任意精度大小的GMP扩展学习
对于各类开发语言来说,整数都有一个最大的位数,如果超过位数就无法显示或者操作了.其实,这也是一种精度越界之后产生的精度丢失问题.在我们的 PHP 代码中,最大的整数非常大,我们可以通过 PHP_INT ...
- CDHtmlDialog探索----WebBrowser扩展和网页Javascript错误处理
当WebBrowser控件(CDHtmlDialog自动创建了WebBrowser控件)加载的网页中含有错误Javascript代码时默认情况下控件会弹出错误信息提示对话框,相对于用户体验来说这样的提 ...
- PHP内核探索之变量(6)- 后续内核探索系列大纲备忘
年前因为工作比较饱和,现在又忙着换工作的事情,基本停止了对博文的更新.后续的博文,还是慢慢补上吧. 为了不至于过于发散,先搞个未成形的大纲,如下: PHP内核探索之变量 不平凡的字符串 PHP内核探 ...
- 使用响应扩展的响应面(Rx)
下载demo - 196 KB 下载source - 98 KB 表的内容 系统要求反应面一个简单的计时器从事件中收集数据序列使用更复杂的查询订阅您希望完成的面最终考虑历史 介绍 "Rx&q ...
- Docker PHP 扩展配置
# PHP 容器配置 # 从官方基础版本构建 FROM php:7.2-fpm # 官方版本默认安装扩展: # Core, ctype, curl # date, dom # fileinfo, fi ...
- Android - 利用扩展函数为Bitmap添加文字水印
<异空间>项目技术分享系列--扩展函数为Bitmap添加文字水印 对图片Bitmap绘制文字水印还是比较常见的需求,毕竟版权意识都在增强(用户可以给自己图片加上用户名),还可以为用户提供更 ...
- Sass介绍及入门教程
Sass是什么? Sass是"Syntactically Awesome StyleSheets"的简称.那么他是什么?其实没有必要太过于纠结,只要知道他是“CSS预处理器”中的一 ...
- 论文翻译:BinaryNet: Training Deep Neural Networks with Weights and Activations Constrained to +1 or −1
目录 摘要 引言 1.BinaryNet 符号函数 梯度计算和累积 通过离散化传播梯度 一些有用的成分 算法1 使用BinaryNet训练DNN 算法2 批量标准化转换(Ioffe和Szegedy,2 ...
- ICML 2018 | 从强化学习到生成模型:40篇值得一读的论文
https://blog.csdn.net/y80gDg1/article/details/81463731 感谢阅读腾讯AI Lab微信号第34篇文章.当地时间 7 月 10-15 日,第 35 届 ...
随机推荐
- x86-TSO : 适用于x86体系架构并发编程的内存模型
Abstract : 如今大数据,云计算,分布式系统等对算力要求高的方向如火如荼.提升计算机算力的一个低成本方法是增加CPU核心,而不是提高单个硬件工作效率. 这就要求软件开发者们能准确,熟悉地运用高 ...
- Hashmap,Set,Map,List,ArrayList的区别
表格: 类型 默认容量 加载因子[1] 扩容增量 底层实现 是否安全及同步方式 Vector 10 1 2倍 Object数组 安全,synchronized ArrayList 10 1 1.5倍( ...
- Map遍历法则
/** * 如果既要遍历key又要value,那么建议这种方式,应为如果先获取keySet然后再执行map.get(key),map内部会执行两次遍历. * 一次是在获取keySet的时候,一次是在遍 ...
- springboot:This application has no explicit mapping for /erro
springboot启动没有报错,但是访问的时候返回如上图的错误.看报错内容感觉是没有这个mapping对应的接口.但是确实写了. 最终发现是因为springboot的启动类放的位置不对.启动类所在的 ...
- 深入理解TypeScript——第一章:上手篇
怎么定义TypeScript呢? TypeScript是一个工具 是一个编译器 编译代码 TypeScript,通过它的能力,默认使用tsc命令,可以根据.ts为后缀名的文件生成一个新的js文件 2. ...
- React代码开发规范
前言 一般在团队开发中每个人的代码习惯都不太一样,这样就会导致代码风格不一致,以致于维护和修改bug的时候看别人的代码成为一种痛苦...这种情况尤其在前端开发中尤为明显.因为关于前端的开发规范貌似也没 ...
- jquery,Datatables插件使用,做根据【日期段】筛选数据的功能 jsp
时间格式为yyyymmdd,通过转换为int类型进行比较大小 画面: jsp代码: 1 //日期显示控件,使用h-ui框架 2 3 <div class="text-c"& ...
- 坐标下降(Coordinate descent)
坐标下降法属于一种非梯度优化的方法,它在每步迭代中沿一个坐标的方向进行线性搜索(线性搜索是不需要求导数的),通过循环使用不同的坐标方法来达到目标函数的局部极小值.
- spring-boot-route(三)实现多文件上传
Spring Boot默认上传的单个文件大小1MB,一次上传的总文件大小为10MB. 单个文件上传使用MultipartFile参数来接收文件,多文件使用MultipartFile[]数组来接收,然后 ...
- python数据结构之图深度优先和广度优先实例详解
本文实例讲述了python数据结构之图深度优先和广度优先用法.分享给大家供大家参考.具体如下: 首先有一个概念:回溯 回溯法(探索与回溯法)是一种选优搜索法,按选优条件向前搜索,以达到目标.但当探索到 ...