Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ARROW-3849: [C++] Leverage Armv8 crc32 extension instructions to accelerate the hash computation for Arm64 #3010

Closed
wants to merge 3 commits into from

Conversation

guyuqi
Copy link
Member

@guyuqi guyuqi commented Nov 22, 2018

The 'Hash utility' leverages SSE4 to accelerate the Crc32 data hash computation for x86.
Correspondingly, we will leverage the Arm crc32 extension instructions
to accelerate the hash computation for Arm64.

  1. Add Arm hardware Crc32 support.
  2. Add the hash computing mode respectively:
  • USE_DEFAULT: Murmur2-64
  • USE_SSE42
  • USE_ARMCRC
  1. Modify the cmake configuration to detect whether the Arm architecture is supported or not on compiling phase. The code will also do a Crc32 run time check(only available for Linux).

@guyuqi
Copy link
Member Author

guyuqi commented Nov 22, 2018

Unit Tests are passed on Arm64 and x86(Xeon E5-2650):

Arm64:

Test project /home/linux/arrow/cpp/bld
      Start  1: allocator-test
 1/39 Test  #1: allocator-test ...................   Passed    0.15 sec
      Start  2: array-test
 2/39 Test  #2: array-test .......................   Passed    5.06 sec
      Start  3: buffer-test
 3/39 Test  #3: buffer-test ......................   Passed    0.14 sec
      Start  4: memory_pool-test
 4/39 Test  #4: memory_pool-test .................   Passed    0.15 sec
      Start  5: pretty_print-test
 5/39 Test  #5: pretty_print-test ................   Passed    0.14 sec
      Start  6: public-api-test
 6/39 Test  #6: public-api-test ..................   Passed    0.15 sec
      Start  7: status-test
 7/39 Test  #7: status-test ......................   Passed    0.14 sec
      Start  8: stl-test
 8/39 Test  #8: stl-test .........................   Passed    0.14 sec
      Start  9: type-test
 9/39 Test  #9: type-test ........................   Passed    0.15 sec
      Start 10: table-test
10/39 Test #10: table-test .......................   Passed    0.14 sec
      Start 11: table_builder-test
11/39 Test #11: table_builder-test ...............   Passed    0.14 sec
      Start 12: tensor-test
12/39 Test #12: tensor-test ......................   Passed    0.14 sec
      Start 13: compute-test
13/39 Test #13: compute-test .....................   Passed    0.79 sec
      Start 14: feather-test
14/39 Test #14: feather-test .....................   Passed    0.35 sec
      Start 15: ipc-read-write-test
15/39 Test #15: ipc-read-write-test ..............   Passed    5.96 sec
      Start 16: ipc-json-test
16/39 Test #16: ipc-json-test ....................   Passed    0.23 sec
      Start 17: json-integration-test
17/39 Test #17: json-integration-test ............   Passed    0.14 sec
      Start 18: csv-chunker-test
18/39 Test #18: csv-chunker-test .................   Passed    0.14 sec
      Start 19: csv-column-builder-test
19/39 Test #19: csv-column-builder-test ..........   Passed    0.15 sec
      Start 20: csv-converter-test
20/39 Test #20: csv-converter-test ...............   Passed    0.14 sec
      Start 21: csv-parser-test
21/39 Test #21: csv-parser-test ..................   Passed    0.14 sec
      Start 22: io-buffered-test
22/39 Test #22: io-buffered-test .................   Passed    0.20 sec
      Start 23: io-compressed-test
23/39 Test #23: io-compressed-test ...............   Passed   13.25 sec
      Start 24: io-file-test
24/39 Test #24: io-file-test .....................   Passed    0.62 sec
      Start 25: io-hdfs-test
25/39 Test #25: io-hdfs-test .....................   Passed    0.15 sec
      Start 26: io-memory-test
26/39 Test #26: io-memory-test ...................   Passed    2.43 sec
      Start 27: io-readahead-test
27/39 Test #27: io-readahead-test ................   Passed    0.68 sec
      Start 28: bit-util-test
28/39 Test #28: bit-util-test ....................   Passed    0.59 sec
      Start 29: checked-cast-test
29/39 Test #29: checked-cast-test ................   Passed    0.14 sec
      Start 30: compression-test
30/39 Test #30: compression-test .................   Passed    0.67 sec
      Start 31: decimal-test
31/39 Test #31: decimal-test .....................   Passed    0.15 sec
      Start 32: key-value-metadata-test
32/39 Test #32: key-value-metadata-test ..........   Passed    0.14 sec
      Start 33: rle-encoding-test
33/39 Test #33: rle-encoding-test ................   Passed    0.42 sec
      Start 34: parsing-util-test
34/39 Test #34: parsing-util-test ................   Passed    0.14 sec
      Start 35: stl-util-test
35/39 Test #35: stl-util-test ....................   Passed    0.14 sec
      Start 36: thread-pool-test
36/39 Test #36: thread-pool-test .................   Passed    0.55 sec
      Start 37: task-group-test
37/39 Test #37: task-group-test ..................   Passed    0.32 sec
      Start 38: lazy-test
38/39 Test #38: lazy-test ........................   Passed    0.14 sec
      Start 39: logging-test
39/39 Test #39: logging-test .....................   Passed    0.56 sec

100% tests passed, 0 tests failed out of 39

x86:

Test project /home/builder/arrow/cpp/bld
      Start  1: allocator-test
 1/40 Test  #1: allocator-test ...................   Passed    0.16 sec
      Start  2: array-test
 2/40 Test  #2: array-test .......................   Passed    2.06 sec
      Start  3: buffer-test
 3/40 Test  #3: buffer-test ......................   Passed    0.14 sec
      Start  4: memory_pool-test
 4/40 Test  #4: memory_pool-test .................   Passed    0.13 sec
      Start  5: pretty_print-test
 5/40 Test  #5: pretty_print-test ................   Passed    0.15 sec
      Start  6: public-api-test
 6/40 Test  #6: public-api-test ..................   Passed    0.17 sec
      Start  7: status-test
 7/40 Test  #7: status-test ......................   Passed    0.15 sec
      Start  8: stl-test
 8/40 Test  #8: stl-test .........................   Passed    0.17 sec
      Start  9: type-test
 9/40 Test  #9: type-test ........................   Passed    0.16 sec
      Start 10: table-test
10/40 Test #10: table-test .......................   Passed    0.16 sec
      Start 11: table_builder-test
11/40 Test #11: table_builder-test ...............   Passed    0.17 sec
      Start 12: tensor-test
12/40 Test #12: tensor-test ......................   Passed    0.16 sec
      Start 13: compute-test
13/40 Test #13: compute-test .....................   Passed    0.46 sec
      Start 14: feather-test
14/40 Test #14: feather-test .....................   Passed    0.29 sec
      Start 15: ipc-read-write-test
15/40 Test #15: ipc-read-write-test ..............   Passed    1.45 sec
      Start 16: ipc-json-test
16/40 Test #16: ipc-json-test ....................   Passed    0.21 sec
      Start 17: json-integration-test
17/40 Test #17: json-integration-test ............   Passed    0.15 sec
      Start 18: csv-chunker-test
18/40 Test #18: csv-chunker-test .................   Passed    0.16 sec
      Start 19: csv-column-builder-test
19/40 Test #19: csv-column-builder-test ..........   Passed    0.16 sec
      Start 20: csv-converter-test
20/40 Test #20: csv-converter-test ...............   Passed    0.17 sec
      Start 21: csv-parser-test
21/40 Test #21: csv-parser-test ..................   Passed    0.15 sec
      Start 22: io-buffered-test
22/40 Test #22: io-buffered-test .................   Passed    0.19 sec
      Start 23: io-compressed-test
23/40 Test #23: io-compressed-test ...............   Passed    4.22 sec
      Start 24: io-file-test
24/40 Test #24: io-file-test .....................   Passed    0.45 sec
      Start 25: io-hdfs-test
25/40 Test #25: io-hdfs-test .....................   Passed    0.15 sec
      Start 26: io-memory-test
26/40 Test #26: io-memory-test ...................   Passed    1.23 sec
      Start 27: io-readahead-test
27/40 Test #27: io-readahead-test ................   Passed    0.31 sec
      Start 28: bit-util-test
28/40 Test #28: bit-util-test ....................   Passed    0.34 sec
      Start 29: checked-cast-test
29/40 Test #29: checked-cast-test ................   Passed    0.16 sec
      Start 30: compression-test
30/40 Test #30: compression-test .................   Passed    0.39 sec
      Start 31: decimal-test
31/40 Test #31: decimal-test .....................   Passed    0.17 sec
      Start 32: key-value-metadata-test
32/40 Test #32: key-value-metadata-test ..........   Passed    0.14 sec
      Start 33: lazy-test
33/40 Test #33: lazy-test ........................   Passed    0.15 sec
      Start 34: logging-test
34/40 Test #34: logging-test .....................   Passed    0.27 sec
      Start 35: parsing-util-test
35/40 Test #35: parsing-util-test ................   Passed    0.15 sec
      Start 36: rle-encoding-test
36/40 Test #36: rle-encoding-test ................   Passed    0.28 sec
      Start 37: stl-util-test
37/40 Test #37: stl-util-test ....................   Passed    0.17 sec
      Start 38: task-group-test
38/40 Test #38: task-group-test ..................   Passed    0.40 sec
      Start 39: thread-pool-test
39/40 Test #39: thread-pool-test .................   Passed    0.84 sec
      Start 40: utf8-util-test
40/40 Test #40: utf8-util-test ...................   Passed    0.18 sec

100% tests passed, 0 tests failed out of 40

@pitrou
Copy link
Member

pitrou commented Nov 22, 2018

Hi @guyuqi,

Thanks a lot for doing this. Could I ask you to base your work on PR #3005? It refactors most of the hashing machinery and also adds some hashing benchmarks that you may be interested to run (and hopefully report results here).

@wesm
Copy link
Member

wesm commented Nov 22, 2018

+1 re basing on #3005. I will work on reviewing that today so we can merge soon

@wesm
Copy link
Member

wesm commented Nov 22, 2018

@guyuqi what arm64 platform / system are you running this one? I wonder if there are public CI services available that we could use for testing on ARM

@guyuqi
Copy link
Member Author

guyuqi commented Nov 23, 2018

+1 re basing on #3005. I will work on reviewing that today so we can merge soon

OK, I'll rebase this PR on #3005.

@wesm
Copy link
Member

wesm commented Nov 23, 2018

This was merged to master so you can rebase on master now

@pitrou pitrou changed the title ARROW-3849: Leverage Armv8 crc32 extension instructions to accelerate the hash computation for Arm64 ARROW-3849: [C++] Leverage Armv8 crc32 extension instructions to accelerate the hash computation for Arm64 Nov 23, 2018
@@ -231,6 +231,26 @@ if (APPLE)
set(CXX_COMMON_FLAGS "${CXX_COMMON_FLAGS} -stdlib=libc++")
endif()

if (CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|AARCH64")
CHECK_CXX_SOURCE_COMPILES("
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

@guyuqi guyuqi Nov 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your comments.
First I request crc extension capabilities from the assembler , and then detect the flags "-march=armv8-a+crc" for Arm64 crc intrinsics. I think it's also a reliable solution. :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the point is, it adds gratuitous complication. Testing for -march=armv8-a+crc should be sufficient.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For not all compilers support the Armv8 crc intrinsics, it is not sufficient just to test for
-march=armv8-a+crc .
If some compiler do not support intrinsics, it is needed to request crc extension capabilities from the assembler. So Arm asm : crc32cx will be tested first, and then we test for -march=armv8-a+crc to detect whether compiler supports intrinsics or not.

1. not support Arm crc
2. support Arm crc, but not support intrinsics
3. support intrinsics

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which compilers don't support intrinsics? Apparently they are a ARM standard.

#define MY_CONFIG_H

/* Support Armv8 CRC instructions */
#cmakedefine ARROW_USE_ARMCE 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file shouldn't be required. Instead you can add preprocessor definitions directly in the cmake files, using e.g. the add_definitions command.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really thanks for comments.
'add_definitions' looks like a switch to enable/disable the macro by the command like
"cmake  -Dxxx=on/off".
In my opinion, it's better to detect whether the macro is supported or not on cmake phase and dynamically to define or not define the Macro related. And it is also convenient and explicit to manage the macro definition.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the doc says, it "Adds definitions to the compiler command line" (emphasis mine). So you can use it to define a preprocessor symbol.

@guyuqi guyuqi force-pushed the ARROW-3849 branch 2 times, most recently from bdcf7b7 to 32f5f4f Compare November 25, 2018 16:02

// define our own implementations of the intrinsics instead.
static inline uint32_t ARMCE_crc32_u8(uint32_t crc, uint8_t value) {
__asm__("crc32cb %w[c], %w[c], %w[v]":[c]"+r"(crc):[v]"r"(value));
Copy link
Member

@pitrou pitrou Nov 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather only use the official intrinsics if possible. Inline assembler adds a maintenance burden.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline asm is for the compilers that do not support intrinsics.

#endif
static inline uint32_t crc32c_runtime_check(void)
{
unsigned long auxv = getauxval(AT_HWCAP);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our style guide mandates 2-space indentation. I recommend you run the style checker using make clang-format.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, thanks. I'll fix it.

@@ -67,14 +76,37 @@ class HashUtil {
return hash;
}

static uint32_t CrcHashARMCE(const void* data, int32_t nbytes, uint32_t hash) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to reconcile the two functions instead of duplicating the code with different function names?
Same below for DoubleCRCHash.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, How about separating the code by using 'Arch macro' in CrcHash/DoubleCRCHash?
Like this:

static uint32_t CrcHash(...) {
.
..
...
  while (p <= end - 4) {
#ifdef  ARROW_HAVE_SSE4_2
      hash = SSE4_crc32_u32(hash, *reinterpret_cast<const uint32_t*>(p));
#elif ARROW_HAVE_ARMCE
      hash = ARMCE_crc32_u64(hash, *reinterpret_cast<const uint64_t*>(p));
#endif
      p += 4;
    }
...
...
.
    }

}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather have the switch outside of the functions. For example you could have:

#ifdef ARROW_HAVE_SSE4_2
#define HW_crc32_u32 SSE4_crc32_u32
#elif defined(ARROW_HAVE_ARMCE)
#define HW_crc32_u32 ARMCE_crc32_u32
#endif

By the way, "ARMCE" doesn't look like a recognized acronym, so I'd rather have e.g. ARROW_HAVE_ARM_CRC.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it makes sense.

template <>
inline int HashUtil::DoubleHash<USE_ARMCRC>(const void* data, int32_t bytes, uint32_t seed) {
// Need run time check
if (crc32c_runtime_check())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for the record, is the runtime check cheap? The hash function will often be called for smallish strings, e.g. 20 to 40 bytes.

Copy link
Member Author

@guyuqi guyuqi Nov 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it mean for "cheap"?
Sorry, I'm not familiar with the hash function calling scenario.
If the hash function is called very frequently, the run time check should be called just once to avoid performance degradation. I will try to move the run time check to a init routine if possible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to find out by running the hashing-benchmark with Arrow compiled in release mode. See if this PR actually gives a speedup :-)
(also, it would be nice to post the results so we non-ARM users can get an idea of the performance we're talking about)

return ScalarHelper<uint64_t, AlgNum>::ComputeHash(h);
} else {
// Fall back on 64-bit Murmur2 for longer strings.
// It has decent speed for medium-sized strings. There may be faster
// hashes on long strings such as xxHash, but that may not matter much
// for the typical length distribution of hash keys.
return HashUtil::MurmurHash2_64(data, static_cast<int>(length), AlgNum);
return HashUtil::Hash<USE_DEFAULT>(data, static_cast<int>(length), AlgNum);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather keep the explicit fallback to MurmurHash here, rather than Hash<USE_DEFAULT>.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ok. I'll follow it.

@guyuqi guyuqi force-pushed the ARROW-3849 branch 3 times, most recently from 423000b to b13ee1f Compare November 28, 2018 07:00
@guyuqi
Copy link
Member Author

guyuqi commented Nov 28, 2018

Update the PR basing on above comments .
Run the hashing-benchmark by -DARROW_BUILD_BENCHMARKS=ON and make runbenchmark :

Average of 10 times:
Enable Arm CRC:

Start 53: hashing-benchmark
12/15 Test #53: hashing-benchmark ................   Passed    4.04 sec

Disable Arm CRC:

Start 53: hashing-benchmark
12/15 Test #53: hashing-benchmark ................   Passed    4.40 sec

It shows ~8% improvement.

I also run the hashing-benchmark with calling run time check.
Result is about 4.04-4.05 sec, and the result with no calling run time check is about 4.03-4.04 sec.

@pitrou
Copy link
Member

pitrou commented Nov 28, 2018

Thanks @guyuqi. Can you run hashing-benchmark directly? It should be somewhere in your build\release directory. The output will be more informative, for example:

$ ./build/release/hashing-benchmark 
2018-11-28 11:26:19
Running ./build/release/hashing-benchmark
Run on (16 X 3000 MHz CPU s)
CPU Caches:
  L1 Data 32K (x8)
  L1 Instruction 64K (x8)
  L2 Unified 512K (x8)
  L3 Unified 8192K (x2)
***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead.
----------------------------------------------------------------------
Benchmark                               Time           CPU Iterations
----------------------------------------------------------------------
BM_HashIntegers/repeats:1              11 us         11 us      61402   13.1776GB/s
BM_HashSmallStrings/repeats:1          74 us         74 us       8937   2.76478GB/s
BM_HashMediumStrings/repeats:1        416 us        416 us       1662   3.11357GB/s
BM_HashLargeStrings/repeats:1         285 us        285 us       2436   6.73363GB/s

@guyuqi
Copy link
Member Author

guyuqi commented Nov 28, 2018

I re-run the hashing-benchmark:
Enable Arm64 CRC32 extension:

linux@wls-arm-qc02:~/yuqi/arrow/cpp/bld/release$ ./hashing-benchmark
2018-11-28 22:59:44
Running ./hashing-benchmark
Run on (46 X 2600 MHz CPU s)
***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead.
----------------------------------------------------------------------
Benchmark                               Time           CPU Iterations
----------------------------------------------------------------------
BM_HashIntegers/repeats:1              12 us         12 us      60570   12.8932GB/s
BM_HashSmallStrings/repeats:1         116 us        116 us       6066   1.75833GB/s
BM_HashMediumStrings/repeats:1        357 us        357 us       1967   3.63539GB/s
BM_HashLargeStrings/repeats:1         187 us        187 us       3676   10.2743GB/s

Disable Arm64 CRC32 extension:

linux@wls-arm-qc02:~/yuqi/arrow/cpp/bld/release$ ./hashing-benchmark
2018-11-28 23:01:25
Running ./hashing-benchmark
Run on (46 X 2600 MHz CPU s)
***WARNING*** CPU scaling is enabled, the benchmark real time measurements may be noisy and will incur extra overhead.
----------------------------------------------------------------------
Benchmark                               Time           CPU Iterations
----------------------------------------------------------------------
BM_HashIntegers/repeats:1              12 us         12 us      59880   12.8938GB/s
BM_HashSmallStrings/repeats:1         126 us        126 us       5532   1.61687GB/s
BM_HashMediumStrings/repeats:1        582 us        582 us       1206   2.22643GB/s
BM_HashLargeStrings/repeats:1         641 us        641 us       1095   2.98781GB/s

It seems HashLargeString case gains a ~ x3.4 performance boost.

@pitrou
Copy link
Member

pitrou commented Nov 28, 2018

Very nice :-)

@pitrou
Copy link
Member

pitrou commented Nov 28, 2018

By the way, which CPU / platform are you testing on?

@pitrou
Copy link
Member

pitrou commented Nov 28, 2018

The code looks good to me now. What's remaining is that you need to fix the style issues spotted in CI:
https://travis-ci.org/apache/arrow/jobs/460631547#L771

To do it locally (rather than wait for CI to validate your changes), best is to call make clang-format (to fix mechanical style issues) and make lint (to check all style rules).

…the hash computation for Arm64

The Hash utility leverages SSE4 to accelerate the Crc32 data hash computation for x86.
Correspondingly, we will leverage the Arm crc32 extension instructions
to accelerate the hash computation for AArch64.

Change-Id: I7da36f8da8d1c32f10eef33a664e5e230f214c59
Signed-off-by: Yuqi Gu <[email protected]>
Change-Id: I3044a89bae619968e340636996f014a0134f1030
Signed-off-by: Yuqi Gu <[email protected]>
@guyuqi guyuqi force-pushed the ARROW-3849 branch 2 times, most recently from d86aee1 to 7ddeb0b Compare November 29, 2018 07:35
Change-Id: I02e8a52935376b4ecc700cd17edaeb871b2bd487
@codecov-io
Copy link

Codecov Report

Merging #3010 into master will increase coverage by 20.74%.
The diff coverage is 100%.

Impacted file tree graph

@@             Coverage Diff             @@
##           master    #3010       +/-   ##
===========================================
+ Coverage   67.32%   88.06%   +20.74%     
===========================================
  Files          58      425      +367     
  Lines        3718    64888    +61170     
===========================================
+ Hits         2503    57146    +54643     
- Misses       1114     7742     +6628     
+ Partials      101        0      -101
Impacted Files Coverage Δ
cpp/src/arrow/util/hash-util.h 96.15% <100%> (ø)
go/arrow/array/table.go
go/arrow/math/uint64_amd64.go
go/arrow/internal/testing/tools/bool.go
go/arrow/internal/bitutil/bitutil.go
go/arrow/memory/memory_avx2_amd64.go
go/arrow/array/null.go
go/arrow/datatype_nested.go
go/arrow/array/string.go
go/arrow/math/uint64_avx2_amd64.go
... and 474 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7800684...6b99d20. Read the comment docs.

@guyuqi
Copy link
Member Author

guyuqi commented Nov 29, 2018

Fix the coding style and rebase PR on master for conflict with #3037 .

@pitrou pitrou closed this in 05c70b0 Nov 29, 2018
@pitrou
Copy link
Member

pitrou commented Nov 29, 2018

Thanks @guyuqi !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants