c++map的使用_在 Cython 项目中使用 abseilcpp

(给Python开发者加星标,提升Python技能)

来源:messense

上一篇文章介绍了使用 CMake 构建 Python C/C++ 扩展的两个方案,也提到了之前在工作中做这个的主要目的是为了能够使用 abseil-cpp[1] 的 Swiss Tables 优化性能,那么这篇文章就来简单介绍一下如何在 Cython 项目中使用 abseil-cpp.

项目目录结构

$ tree -L 2 .
.
├── CMakeLists.txt        # 项目主 CMake 配置
├── absl.pxd              # abseil-cpp 接口的部分 Cython 定义文件
├── hello                 # hello Python 库
│   ├── CMakeLists.txt    # Python C++ 扩展 CMake 配置
│   ├── __init__.py
│   └── _hello.pyx        # Cython 文件
├── pyproject.toml
├── setup.py
├── src
│   ├── CMakeLists.txt    # 项目 C/C++ 代码 CMake 配置
│   ├── absl.cc
│   └── absl.h
├── test_hello_cython.py  # 测试代码
├── vendor                # C/C++ 第三方库放置路径
│   └── abseil-cpp        # abseil-cpp Git submodule
└── venv                  # Python virtualenv

该示例项目的代码托管在 GitHub:https://github.com/messense/cython-abseil-sample[2]

接入 abseil-cpp

可以直接把 abseil-cpp 作为一个 Git submodule 导入到自己的项目中

git submodule add --depth 50 https://github.com/abseil/abseil-cpp.git vendor/abseil-cpp

这里我把它放到了 vendor/abseil-cpp 目录中,然后可以在项目主 CMakeLists.txt 文件中配置依赖,官方文档[3] 上有更详细的说明。

cmake_minimum_required(VERSION 3.5.0)
project(hello)

set (CMAKE_POSITION_INDEPENDENT_CODE ON)

# Abseil requires C++11
set(CMAKE_CXX_STANDARD 11)

# Process Abseil's CMake build system
add_subdirectory(vendor/abseil-cpp)
include_directories(
/usr/local/include
${CMAKE_CURRENT_SOURCE_DIR}/vendor/abseil-cpp
${CMAKE_CURRENT_SOURCE_DIR}/src)

add_subdirectory(src)
add_subdirectory(hello)

上面的配置把 src/ 和 hello/ 目录也作为 CMake 子目录加入了配置,下面分别介绍一下。

src 目录 - C/C++ 模块

src 目录主要存放了该项目点 C/C++ 代码,这部分代码和是 Python 无关的。

CMakeLists.txt

set(SOURCES absl.cc)
add_library(hello_absl STATIC ${SOURCES})
target_link_libraries(hello_absl absl::flat_hash_map)

这里的构建配置非常简单,只是构建了一个静态的 hello_absl target 并链接 absl::flat_hash_map 库,这个 target 暴露的 public API 只有一个返回值类型为 absl::flat_hash_map 的函数 fn_return_flat_hash_map,定义在 src/absl.h 文件中:

#include 
#include "absl/container/flat_hash_map.h"

absl::flat_hash_map<int, std::string> fn_return_flat_hash_map(void);

其实现也非常简单 absl.cc:

#include "absl.h"

absl::flat_hash_map<int, std::string> fn_return_flat_hash_map(void) {
    absl::flat_hash_map<int, std::string> numbers =
        {{1, "one"}, {2, "two"}, {3, "three"}};
    return numbers;
}

hello 目录 - Cython 模块

hello 目录存放的是 Python 模块代码,包含一个 _hello.pyx 的 Cython 模块。

CMakeLists.txt

if(SKBUILD)
message(STATUS "The project is built using scikit-build")
find_package(PythonExtensions REQUIRED)
find_package(Cython REQUIRED)

include_directories(
/usr/local/include
${PYTHON_INCLUDE_DIRS}
${CMAKE_CURRENT_SOURCE_DIR}/vendor/abseil-cpp)

add_cython_target(_hello CXX)
add_library(_hello MODULE ${_hello})
target_link_libraries(_hello hello_absl)
python_extension_module(_hello)

install(TARGETS _hello LIBRARY DESTINATION hello)
endif()
  1. 增加 SKBUILD 变量的判断,只有在使用 scikit-build 构建的时候才会去构建 Python 扩展,这样如果你的项目中的 C/C++ 部分不依赖 Python 也可以运行、测试或者 benchmark 的话,会比较方便
  2. 增加了 find_package(Cython REQUIRED) 和 add_cython_target(_hello CXX) 配置 Cython 模块支持

_hello.pyx

from libcpp.string cimport string

from absl cimport flat_hash_map

cdef extern from "absl.h":
    cdef flat_hash_map[int, string] fn_return_flat_hash_map() nogil

cpdef dict hello_absl():
    cdef flat_hash_map[int, string] a_map
    with nogil:
      a_map = fn_return_flat_hash_map()
    return {it.first: it.second.decode() for it in a_map}
  1. 从 Cython 自带的 libcpp 标准库导入了 std::string
  2. 从我们定义的 absl.pxd 导入了 flat_hash_mapabsl.pxd 详情下面再解释
  3. 使用 cdef extern 定义了上述 src 目录中 C++ 模块定义的 fn_return_flat_hash_map 函数原型
  4. 在 cpdef dict hello_absl 函数中调用 fn_return_flat_hash_map C++ 函数并转换成 Python dict 返回

这里有两个问题需要解答:

  1. from absl cimport flat_hash_map 如何工作的?
  2. flat_hash_map 转换成 Python dict 为什么不能像 libcpp 的 unordered_map 一样直接赋值给 dict 类型的变量自动转换而需要迭代手动生成一个 dict?

问题 1 我们需要了解一下 Cython 是如何调用 C/C++ 函数的,官方文档中关于 pxd 文件的文档[4]有如下解释

Cython uses .pxd files which work like C header files – they contain Cython declarations (and sometimes code sections) which are only meant for inclusion by Cython modules. A pxd file is imported into a pyx module by using the cimport keyword.

为什么 from libcpp.string import string 不需要我们提供 pxd 文件呢?这是因为 Cython 已经默认包含了 libcpp 的接口定义[5],但是 abseil-cpp 的接口定义 Cython 默认是没有的,所以需要提供我们,因为我们这里只用到了 abseil-cpp 的 flat_hash_map 类,故 absl.pxd 中目前只有它的定义,如果以后用到其他的类,也可以在里面增加定义。

absl.pxd

absl.pxd 的内容是从 Cython 自带的 libcpp 代码中的 unordered_map.pxd 代码修改的,diff 一下这两个文件

--- unordered_map.pxd 2020-05-10 16:29:28.000000000 +0800
+++ absl.pxd 2020-05-10 14:54:27.000000000 +0800
@@ -1,7 +1,7 @@
-from .utility cimport pair
+from libcpp.utility cimport pair
 
-cdef extern from "" namespace "std" nogil:
-    cdef cppclass unordered_map[T, U]:
+cdef extern from "absl/container/flat_hash_map.h" namespace "absl" nogil:
+    cdef cppclass flat_hash_map[T, U]:
         ctypedef T key_type
         ctypedef U mapped_type
         ctypedef pair[const T, U] value_type
@@ -21,17 +21,17 @@
             pass
         cppclass const_reverse_iterator(reverse_iterator):
             pass
-        unordered_map() except +
-        unordered_map(unordered_map&) except +
-        #unordered_map(key_compare&)
+        flat_hash_map() except +
+        flat_hash_map(flat_hash_map&) except +
+        #flat_hash_map(key_compare&)
         U& operator[](T&)
-        #unordered_map& operator=(unordered_map&)
-        bint operator==(unordered_map&, unordered_map&)
-        bint operator!=(unordered_map&, unordered_map&)
-        bint operator-        bint operator>(unordered_map&, unordered_map&)-        bint operator<=(unordered_map&, unordered_map&)-        bint operator>=(unordered_map&, unordered_map&)+        #flat_hash_map& operator=(flat_hash_map&)+        bint operator==(flat_hash_map&, flat_hash_map&)+        bint operator!=(flat_hash_map&, flat_hash_map&)+        bint operator+        bint operator>(flat_hash_map&, flat_hash_map&)+        bint operator<=(flat_hash_map&, flat_hash_map&)+        bint operator>=(flat_hash_map&, flat_hash_map&)
         U& at(const T&)
         const U& const_at "at"(const T&)
         iterator begin()@@ -43,8 +43,8 @@
         const_iterator const_end "end"()
         pair[iterator, iterator] equal_range(T&)
         pair[const_iterator, const_iterator] const_equal_range "equal_range"(const T&)-        iterator erase(iterator)-        iterator erase(iterator, iterator)+        void erase(iterator)+        void erase(iterator, iterator)
         size_t erase(T&)
         iterator find(T&)
         const_iterator const_find "find"(T&)@@ -60,7 +60,7 @@
         reverse_iterator rend()
         const_reverse_iterator const_rend "rend"()
         size_t size()-        void swap(unordered_map&)+        void swap(flat_hash_map&)
         iterator upper_bound(T&)
         const_iterator const_upper_bound "upper_bound"(T&)
         #value_compare value_comp()

可以发现,除了将 unordered_map 名字替换成 flat_hash_map 和修改 extern from 路径外,只有几个 erase 方法的返回值类型不一样,unordered_map.erase 会返回一个新的 iterator 而 flat_hash_map.erase 返回值类型为 void,这是因为 abseil-cpp 的 flat_hash_map 为了优化 erase 方法的性能和 C++ STL 的 unordered_map erase 方法行为有所不同导致的:

Guarantees an O(1) erase method by returning void instead of an iterator.

问题 2 则是因为 Cython 编译器对 libcpp 有特殊支持会生成很多转换函数,而我们自行提供的 .pxd 文件则没有这种特殊待遇导致的。有兴趣的可以看一下 Cython.Compiler.PyrexTypes 模块中的 CTypedefType.create_to_py_utility_code[6] 方法代码。

本文只给出了一个 C++ 代码返回值类型为 abseil-cpp 类型的例子,实际上也可以扩展为函数参数类型为 abseil-cpp 或者其他任何 C/C++ 第三方库的类型,这里不再赘述。

测试

test_hello_cython.py 文件中编写了一个 test_hello_absl 测试用例:

import hello


def test_hello_absl():
    ret = hello.hello_absl()
    assert ret == {1: "one", 2: "two", 3: "three"}

运行 pytest -v 可以确认测试正常:

$ pytest -v
test_hello_cython.py::test_hello_absl PASSED

参考资料

[1] abseil-cp p:  https://abseil.io/docs/cpp [2] cython-abseil-sample:  https://github.com/messense/cython-abseil-sample [3] abseil-cpp CMake 接入官方文档:  https://abseil.io/docs/cpp/quickstart-cmake #creating-your-cmakeliststxt-file [4] Cython pxd 文件文档:  https://cython.readthedocs.io/en/latest/src/tutorial/pxd_files.html [5] Cython libcpp 接口定义:  https://cython.readthedocs.io/en/latest/src/userguide/wrapping_CPlusPlus.html#standard-library [6] CTypedefType.create_to_py_utility_code:  https://github.com/cython/cython/blo b/5b6bbdab8387666bce5e11975e1e0baa0081a534/Cython/Compiler/PyrexTypes.py#L470 推荐阅读   点击标题可跳转

Cython 三分钟入门

用 Python 写出 Gameboy 模拟器,还能训练 AI 模型:丹麦小哥的大学项目火了

PyTorch 版 EfficientDet 比官方 TF 实现快 25 倍?这个 GitHub 项目数天狂揽千星

觉得本文对你有帮助?请分享给更多人

关注「Python开发者」加星标,提升Python技能

cd407f8bde23cfaebd5488aa0255b70e.png

好文章,我在看❤️

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值