Runtime dispatch

* Attempt 1 - fn targeting

GCC won't work with templates with different targets, need to specialize all the way up the call stack.

* Compiles properly with cmake. Does not with the Makefile.

* Compilation works with Makefile

* instruction_set changes to architecture

* some aesthetic changes

* fix amalgation and tests + aesthetic changes

* This now compiles and passes tests under CLANG

* Minor correction.

* Trying to make it work on ARM

* Adding missing namespace

* Missing bracket

* Fixing minor compilation issues.

* Getting parse to use runtime dispatch

* Fixing amalgamation script.

* Making sure that NEON is supported.

* Fixing typo

* Merging https://github.com/lemire/simdjson/pull/229

* Manual merge of
https://github.com/lemire/simdjson/pull/229
by @jkeiser  (second part)

* Trying another way.

* Removing the paral.

* Fixing the make file

* Let us make the practice run long enough.

* Resolved the awful slowness.

* Cleaning the README.md

* With runtime dispatching, we should not need flags anymore.

* Changing isa detection file's name + fixing typos.
This commit is contained in:
Daniel Lemire
2019-07-28 22:46:33 -04:00
committed by ioioioio
parent bcabdfc1ae
commit eba02dc1b9
39 changed files with 4409 additions and 3663 deletions
+24
View File
@@ -63,6 +63,30 @@ size_t jsonminify(const unsigned char *bytes, size_t howmany,
#include <cstring>
namespace simdjson {
// some intrinsics are missing under GCC?
#ifndef __clang__
#ifndef _MSC_VER
static __m256i inline _mm256_loadu2_m128i(__m128i const *__addr_hi,
__m128i const *__addr_lo) {
__m256i __v256 = _mm256_castsi128_si256(_mm_loadu_si128(__addr_lo));
return _mm256_insertf128_si256(__v256, _mm_loadu_si128(__addr_hi), 1);
}
static inline void _mm256_storeu2_m128i(__m128i *__addr_hi, __m128i *__addr_lo,
__m256i __a) {
__m128i __v128;
__v128 = _mm256_castsi256_si128(__a);
_mm_storeu_si128(__addr_lo, __v128);
__v128 = _mm256_extractf128_si256(__a, 1);
_mm_storeu_si128(__addr_hi, __v128);
}
#endif
#endif
// a straightforward comparison of a mask against input.
static uint64_t cmp_mask_against_input_mini(__m256i input_lo, __m256i input_hi,
__m256i mask) {
+28 -35
View File
@@ -6,52 +6,45 @@
#include <unistd.h>
#endif
#include "simdjson/simdjson.h"
#include "simdjson/isadetection.h"
#include "simdjson/portability.h"
namespace simdjson {
architecture find_best_supported_implementation() {
constexpr uint32_t haswell_flags = SIMDExtensions::AVX2 | SIMDExtensions::PCLMULQDQ
| SIMDExtensions::BMI1 | SIMDExtensions::BMI2;
constexpr uint32_t westmere_flags = SIMDExtensions::SSE42 | SIMDExtensions::PCLMULQDQ;
uint32_t supports = detect_supported_architectures();
// Order from best to worst (within architecture)
if ((haswell_flags & supports) == haswell_flags) return architecture::haswell;
if ((westmere_flags & supports) == westmere_flags) return architecture::westmere;
if (SIMDExtensions::NEON) return architecture::arm64;
return architecture::none;
}
// Responsible to select the best json_parse implementation
int json_parse_dispatch(const uint8_t *buf, size_t len, ParsedJson &pj, bool reallocifneeded) {
// Versions for each implementation
#ifdef __AVX2__
json_parse_functype* avx_implementation = &json_parse_implementation<instruction_set::avx2>;
#endif
#if defined(__SSE4_2__) || (defined(_MSC_VER) && defined(_M_AMD64))
json_parse_functype* sse4_2_implementation = &json_parse_implementation<instruction_set::sse4_2>;
#endif
#if defined(__ARM_NEON) || (defined(_MSC_VER) && defined(_M_ARM64))
json_parse_functype* neon_implementation = &json_parse_implementation<instruction_set::neon>;
#endif
// Determining which implementation is the more suitable
// Should be done at runtime. Does not make any sense on preprocessor.
#ifdef __AVX2__
instruction_set best_implementation = instruction_set::avx2;
#elif defined (__SSE4_2__) || (defined(_MSC_VER) && defined(_M_AMD64))
instruction_set best_implementation = instruction_set::sse4_2;
#elif defined (__ARM_NEON) || (defined(_MSC_VER) && defined(_M_ARM64))
instruction_set best_implementation = instruction_set::neon;
#else
instruction_set best_implementation = instruction_set::none;
#endif
architecture best_implementation = find_best_supported_implementation();
// Selecting the best implementation
switch (best_implementation) {
#ifdef __AVX2__
case instruction_set::avx2 :
json_parse_ptr = avx_implementation;
#ifdef IS_X86_64
case architecture::haswell:
json_parse_ptr = &json_parse_implementation<architecture::haswell>;
break;
case architecture::westmere:
json_parse_ptr = &json_parse_implementation<architecture::westmere>;
break;
#endif
#if defined(__SSE4_2__) || (defined(_MSC_VER) && defined(_M_AMD64))
case instruction_set::sse4_2 :
json_parse_ptr = sse4_2_implementation;
break;
#endif
#if defined(__ARM_NEON) || (defined(_MSC_VER) && defined(_M_ARM64))
case instruction_set::neon :
json_parse_ptr = neon_implementation;
#ifdef IS_ARM64
case architecture::arm64:
json_parse_ptr = &json_parse_implementation<architecture::arm64>;
break;
#endif
default :
std::cerr << "No implemented simd instruction set supported" << std::endl;
std::cerr << "The processor is not supported by simdjson." << std::endl;
return simdjson::UNEXPECTED_ERROR;
}
+37 -1
View File
@@ -1 +1,37 @@
// File kept in case we want to reuse it soon. (many configuration files to edit)
#include "simdjson/portability.h"
#ifdef IS_X86_64
#include "simdjson/stage1_find_marks_haswell.h"
#include "simdjson/stage1_find_marks_westmere.h"
TARGET_HASWELL
namespace simdjson {
template<>
int find_structural_bits<architecture::haswell>(const uint8_t *buf, size_t len, ParsedJson &pj) {
FIND_STRUCTURAL_BITS(architecture::haswell, buf, len, pj);
}
} // simdjson
UNTARGET_REGION
TARGET_WESTMERE
namespace simdjson {
template<>
int find_structural_bits<architecture::westmere>(const uint8_t *buf, size_t len, ParsedJson &pj) {
FIND_STRUCTURAL_BITS(architecture::westmere, buf, len, pj);
}
} // simdjson
UNTARGET_REGION
#endif
#ifdef IS_ARM64
#include "simdjson/stage1_find_marks_arm64.h"
namespace simdjson {
template<>
int find_structural_bits<architecture::arm64>(const uint8_t *buf, size_t len, ParsedJson &pj) {
FIND_STRUCTURAL_BITS(architecture::arm64, buf, len, pj);
}
}
#endif
+534 -1
View File
@@ -1 +1,534 @@
// File kept in case we want to reuse it soon. (many configuration files to edit)
#include "simdjson/stage2_build_tape.h"
namespace simdjson {
// this macro reads the next structural character, updating idx, i and c.
#define UPDATE_CHAR() \
{ \
idx = pj.structural_indexes[i++]; \
c = buf[idx]; \
}
#ifdef SIMDJSON_USE_COMPUTED_GOTO
#define SET_GOTO_ARRAY_CONTINUE() pj.ret_address[depth] = &&array_continue;
#define SET_GOTO_OBJECT_CONTINUE() pj.ret_address[depth] = &&object_continue;
#define SET_GOTO_START_CONTINUE() pj.ret_address[depth] = &&start_continue;
#define GOTO_CONTINUE() goto *pj.ret_address[depth];
#else
#define SET_GOTO_ARRAY_CONTINUE() pj.ret_address[depth] = 'a';
#define SET_GOTO_OBJECT_CONTINUE() pj.ret_address[depth] = 'o';
#define SET_GOTO_START_CONTINUE() pj.ret_address[depth] = 's';
#define GOTO_CONTINUE() { \
if(pj.ret_address[depth] == 'a') { \
goto array_continue; \
} else if (pj.ret_address[depth] == 'o') { \
goto object_continue; \
} else { \
goto start_continue; \
} \
}
#endif
/************
* The JSON is parsed to a tape, see the accompanying tape.md file
* for documentation.
***********/
// We need to compile that code for multiple architectures. However, target attributes can be used
// only once by function definition. Huge macro seemed better than huge code duplication.
// int UNIFIED_MACHINE(const uint8_t *buf, size_t len, ParsedJson &pj)
#define UNIFIED_MACHINE(T, buf, len, pj) { \
if (ALLOW_SAME_PAGE_BUFFER_OVERRUN) { \
memset((uint8_t*)buf + len, 0, SIMDJSON_PADDING); /* to please valgrind */ \
} \
uint32_t i = 0; /* index of the structural character (0,1,2,3...) */ \
uint32_t idx; /* location of the structural character in the input (buf) */ \
uint8_t c; /* used to track the (structural) character we are looking at, updated */ \
/* by UPDATE_CHAR macro */ \
uint32_t depth = 0; /* could have an arbitrary starting depth */ \
pj.init(); /* sets isvalid to false */ \
if(pj.bytecapacity < len) { \
pj.errorcode = simdjson::CAPACITY; \
return pj.errorcode; \
} \
\
/*//////////////////////////// START STATE ///////////////////////////// */ \
SET_GOTO_START_CONTINUE() \
pj.containing_scope_offset[depth] = pj.get_current_loc(); \
pj.write_tape(0, 'r'); /* r for root, 0 is going to get overwritten */ \
/* the root is used, if nothing else, to capture the size of the tape */ \
depth++; /* everything starts at depth = 1, depth = 0 is just for the root, the root may contain an object, an array or something else. */ \
if (depth >= pj.depthcapacity) { \
goto fail; \
} \
\
UPDATE_CHAR(); \
switch (c) { \
case '{': \
pj.containing_scope_offset[depth] = pj.get_current_loc(); \
SET_GOTO_START_CONTINUE(); \
depth++; \
if (depth >= pj.depthcapacity) { \
goto fail; \
} \
pj.write_tape(0, c); /* strangely, moving this to object_begin slows things down */ \
goto object_begin; \
case '[': \
pj.containing_scope_offset[depth] = pj.get_current_loc(); \
SET_GOTO_START_CONTINUE(); \
depth++; \
if (depth >= pj.depthcapacity) { \
goto fail; \
} \
pj.write_tape(0, c); \
goto array_begin; \
/* #define SIMDJSON_ALLOWANYTHINGINROOT */ \
/* A JSON text is a serialized value. Note that certain previous */ \
/* specifications of JSON constrained a JSON text to be an object or an */ \
/* array. Implementations that generate only objects or arrays where a */ \
/* JSON text is called for will be interoperable in the sense that all */ \
/* implementations will accept these as conforming JSON texts. */ \
/* https://tools.ietf.org/html/rfc8259 */ \
/* #ifdef SIMDJSON_ALLOWANYTHINGINROOT */ \
case '"': { \
if (!parse_string<T>(buf, len, pj, depth, idx)) { \
goto fail; \
} \
break; \
} \
case 't': { \
/* we need to make a copy to make sure that the string is space terminated. */ \
/* this only applies to the JSON document made solely of the true value. */ \
/* this will almost never be called in practice */ \
char * copy = static_cast<char *>(malloc(len + SIMDJSON_PADDING)); \
if(copy == nullptr) { \
goto fail; \
} \
memcpy(copy, buf, len); \
copy[len] = ' '; \
if (!is_valid_true_atom(reinterpret_cast<const uint8_t *>(copy) + idx)) { \
free(copy); \
goto fail; \
} \
free(copy); \
pj.write_tape(0, c); \
break; \
} \
case 'f': { \
/* we need to make a copy to make sure that the string is space terminated. */ \
/* this only applies to the JSON document made solely of the false value. */ \
/* this will almost never be called in practice */ \
char * copy = static_cast<char *>(malloc(len + SIMDJSON_PADDING)); \
if(copy == nullptr) { \
goto fail; \
} \
memcpy(copy, buf, len); \
copy[len] = ' '; \
if (!is_valid_false_atom(reinterpret_cast<const uint8_t *>(copy) + idx)) { \
free(copy); \
goto fail; \
} \
free(copy); \
pj.write_tape(0, c); \
break; \
} \
case 'n': { \
/* we need to make a copy to make sure that the string is space terminated. */ \
/* this only applies to the JSON document made solely of the null value. */ \
/* this will almost never be called in practice */ \
char * copy = static_cast<char *>(malloc(len + SIMDJSON_PADDING)); \
if(copy == nullptr) { \
goto fail; \
} \
memcpy(copy, buf, len); \
copy[len] = ' '; \
if (!is_valid_null_atom(reinterpret_cast<const uint8_t *>(copy) + idx)) { \
free(copy); \
goto fail; \
} \
free(copy); \
pj.write_tape(0, c); \
break; \
} \
case '0': \
case '1': \
case '2': \
case '3': \
case '4': \
case '5': \
case '6': \
case '7': \
case '8': \
case '9': { \
/* we need to make a copy to make sure that the string is space terminated. */ \
/* this is done only for JSON documents made of a sole number */ \
/* this will almost never be called in practice. We terminate with a space */ \
/* because we do not want to allow NULLs in the middle of a number (whereas a */ \
/* space in the middle of a number would be identified in stage 1). */ \
char * copy = static_cast<char *>(malloc(len + SIMDJSON_PADDING)); \
if(copy == nullptr) { \
goto fail; \
} \
memcpy(copy, buf, len); \
copy[len] = ' '; \
if (!parse_number(reinterpret_cast<const uint8_t *>(copy), pj, idx, false)) { \
free(copy); \
goto fail; \
} \
free(copy); \
break; \
} \
case '-': { \
/* we need to make a copy to make sure that the string is NULL terminated. */ \
/* this is done only for JSON documents made of a sole number */ \
/* this will almost never be called in practice */ \
char * copy = static_cast<char *>(malloc(len + SIMDJSON_PADDING)); \
if(copy == nullptr) { \
goto fail; \
} \
memcpy(copy, buf, len); \
copy[len] = ' '; \
if (!parse_number(reinterpret_cast<const uint8_t *>(copy), pj, idx, true)) { \
free(copy); \
goto fail; \
} \
free(copy); \
break; \
} \
/* #endif // ALLOWANYTHINGINROOT */ \
default: \
goto fail; \
} \
start_continue: \
/* the string might not be NULL terminated. */ \
if(i + 1 == pj.n_structural_indexes) { \
goto succeed; \
} else { \
goto fail; \
} \
/*//////////////////////////// OBJECT STATES ///////////////////////////// */ \
\
object_begin: \
UPDATE_CHAR(); \
switch (c) { \
case '"': { \
if (!parse_string<T>(buf, len, pj, depth, idx)) { \
goto fail; \
} \
goto object_key_state; \
} \
case '}': \
goto scope_end; /* could also go to object_continue */ \
default: \
goto fail; \
} \
\
object_key_state: \
UPDATE_CHAR(); \
if (c != ':') { \
goto fail; \
} \
UPDATE_CHAR(); \
switch (c) { \
case '"': { \
if (!parse_string<T>(buf, len, pj, depth, idx)) { \
goto fail; \
} \
break; \
} \
case 't': \
if (!is_valid_true_atom(buf + idx)) { \
goto fail; \
} \
pj.write_tape(0, c); \
break; \
case 'f': \
if (!is_valid_false_atom(buf + idx)) { \
goto fail; \
} \
pj.write_tape(0, c); \
break; \
case 'n': \
if (!is_valid_null_atom(buf + idx)) { \
goto fail; \
} \
pj.write_tape(0, c); \
break; \
case '0': \
case '1': \
case '2': \
case '3': \
case '4': \
case '5': \
case '6': \
case '7': \
case '8': \
case '9': { \
if (!parse_number(buf, pj, idx, false)) { \
goto fail; \
} \
break; \
} \
case '-': { \
if (!parse_number(buf, pj, idx, true)) { \
goto fail; \
} \
break; \
} \
case '{': { \
pj.containing_scope_offset[depth] = pj.get_current_loc(); \
pj.write_tape(0, c); /* here the compilers knows what c is so this gets optimized */ \
/* we have not yet encountered } so we need to come back for it */ \
SET_GOTO_OBJECT_CONTINUE() \
/* we found an object inside an object, so we need to increment the depth */ \
depth++; \
if (depth >= pj.depthcapacity) { \
goto fail; \
} \
\
goto object_begin; \
} \
case '[': { \
pj.containing_scope_offset[depth] = pj.get_current_loc(); \
pj.write_tape(0, c); /* here the compilers knows what c is so this gets optimized */ \
/* we have not yet encountered } so we need to come back for it */ \
SET_GOTO_OBJECT_CONTINUE() \
/* we found an array inside an object, so we need to increment the depth */ \
depth++; \
if (depth >= pj.depthcapacity) { \
goto fail; \
} \
goto array_begin; \
} \
default: \
goto fail; \
} \
\
object_continue: \
UPDATE_CHAR(); \
switch (c) { \
case ',': \
UPDATE_CHAR(); \
if (c != '"') { \
goto fail; \
} else { \
if (!parse_string<T>(buf, len, pj, depth, idx)) { \
goto fail; \
} \
goto object_key_state; \
} \
case '}': \
goto scope_end; \
default: \
goto fail; \
} \
\
/*//////////////////////////// COMMON STATE ///////////////////////////// */ \
\
scope_end: \
/* write our tape location to the header scope */ \
depth--; \
pj.write_tape(pj.containing_scope_offset[depth], c); \
pj.annotate_previousloc(pj.containing_scope_offset[depth], \
pj.get_current_loc()); \
/* goto saved_state */ \
GOTO_CONTINUE() \
\
/*//////////////////////////// ARRAY STATES ///////////////////////////// */ \
array_begin: \
UPDATE_CHAR(); \
if (c == ']') { \
goto scope_end; /* could also go to array_continue */ \
} \
\
main_array_switch: \
/* we call update char on all paths in, so we can peek at c on the */ \
/* on paths that can accept a close square brace (post-, and at start) */ \
switch (c) { \
case '"': { \
if (!parse_string<T>(buf, len, pj, depth, idx)) { \
goto fail; \
} \
break; \
} \
case 't': \
if (!is_valid_true_atom(buf + idx)) { \
goto fail; \
} \
pj.write_tape(0, c); \
break; \
case 'f': \
if (!is_valid_false_atom(buf + idx)) { \
goto fail; \
} \
pj.write_tape(0, c); \
break; \
case 'n': \
if (!is_valid_null_atom(buf + idx)) { \
goto fail; \
} \
pj.write_tape(0, c); \
break; /* goto array_continue; */ \
\
case '0': \
case '1': \
case '2': \
case '3': \
case '4': \
case '5': \
case '6': \
case '7': \
case '8': \
case '9': { \
if (!parse_number(buf, pj, idx, false)) { \
goto fail; \
} \
break; /* goto array_continue; */ \
} \
case '-': { \
if (!parse_number(buf, pj, idx, true)) { \
goto fail; \
} \
break; /* goto array_continue; */ \
} \
case '{': { \
/* we have not yet encountered ] so we need to come back for it */ \
pj.containing_scope_offset[depth] = pj.get_current_loc(); \
pj.write_tape(0, c); /* here the compilers knows what c is so this gets optimized */ \
SET_GOTO_ARRAY_CONTINUE() \
/* we found an object inside an array, so we need to increment the depth */ \
depth++; \
if (depth >= pj.depthcapacity) { \
goto fail; \
} \
\
goto object_begin; \
} \
case '[': { \
/* we have not yet encountered ] so we need to come back for it */ \
pj.containing_scope_offset[depth] = pj.get_current_loc(); \
pj.write_tape(0, c); /* here the compilers knows what c is so this gets optimized */ \
SET_GOTO_ARRAY_CONTINUE() \
/* we found an array inside an array, so we need to increment the depth */ \
depth++; \
if (depth >= pj.depthcapacity) { \
goto fail; \
} \
goto array_begin; \
} \
default: \
goto fail; \
} \
\
array_continue: \
UPDATE_CHAR(); \
switch (c) { \
case ',': \
UPDATE_CHAR(); \
goto main_array_switch; \
case ']': \
goto scope_end; \
default: \
goto fail; \
} \
\
/*//////////////////////////// FINAL STATES ///////////////////////////// */ \
\
succeed: \
depth --; \
if(depth != 0) { \
fprintf(stderr, "internal bug\n"); \
abort(); \
} \
if(pj.containing_scope_offset[depth] != 0) { \
fprintf(stderr, "internal bug\n"); \
abort(); \
} \
pj.annotate_previousloc(pj.containing_scope_offset[depth], \
pj.get_current_loc()); \
pj.write_tape(pj.containing_scope_offset[depth], 'r'); /* r is root */ \
\
pj.isvalid = true; \
pj.errorcode = simdjson::SUCCESS; \
return pj.errorcode; \
fail: \
/* we do not need the next line because this is done by pj.init(), pessimistically. */ \
/* pj.isvalid = false; */ \
/* At this point in the code, we have all the time in the world. */ \
/* Note that we know exactly where we are in the document so we could, */ \
/* without any overhead on the processing code, report a specific location. */ \
/* We could even trigger special code paths to assess what happened carefully, */ \
/* all without any added cost. */ \
if (depth >= pj.depthcapacity) { \
pj.errorcode = simdjson::DEPTH_ERROR; \
return pj.errorcode; \
} \
switch(c) { \
case '"': \
pj.errorcode = simdjson::STRING_ERROR; \
return pj.errorcode; \
case '0': \
case '1': \
case '2': \
case '3': \
case '4': \
case '5': \
case '6': \
case '7': \
case '8': \
case '9': \
case '-': \
pj.errorcode = simdjson::NUMBER_ERROR; \
return pj.errorcode; \
case 't': \
pj.errorcode = simdjson::T_ATOM_ERROR; \
return pj.errorcode; \
case 'n': \
pj.errorcode = simdjson::N_ATOM_ERROR; \
return pj.errorcode; \
case 'f': \
pj.errorcode = simdjson::F_ATOM_ERROR; \
return pj.errorcode; \
default: \
break; \
} \
pj.errorcode = simdjson::TAPE_ERROR; \
return pj.errorcode; \
} \
}
#ifdef IS_X86_64
TARGET_HASWELL
namespace simdjson {
template<>
WARN_UNUSED ALLOW_SAME_PAGE_BUFFER_OVERRUN_QUALIFIER LENIENT_MEM_SANITIZER
int unified_machine<architecture::haswell>(const uint8_t *buf, size_t len, ParsedJson &pj) {
UNIFIED_MACHINE(architecture::haswell, buf, len, pj);
}
}
UNTARGET_REGION
TARGET_WESTMERE
namespace simdjson {
template<>
WARN_UNUSED ALLOW_SAME_PAGE_BUFFER_OVERRUN_QUALIFIER LENIENT_MEM_SANITIZER
int unified_machine<architecture::westmere>(const uint8_t *buf, size_t len, ParsedJson &pj) {
UNIFIED_MACHINE(architecture::westmere, buf, len, pj);
}
}
UNTARGET_REGION
#endif // IS_X86_64
#ifdef IS_ARM64
namespace simdjson {
template<>
WARN_UNUSED ALLOW_SAME_PAGE_BUFFER_OVERRUN_QUALIFIER LENIENT_MEM_SANITIZER
int unified_machine<architecture::arm64>(const uint8_t *buf, size_t len, ParsedJson &pj) {
UNIFIED_MACHINE(architecture::arm64, buf, len, pj);
}
}
#endif