Fast parsing HTTP verbs

Author:Wojciech Muła
Added on:2022-01-29

Contents

Introduction

When we started to use boost::beast library at work, obviously I downloaded its source code. Can't say I am good at navigation across boost libraries, I was just opening random files waiting for compilation completion. My attention was caught by procedure string_to_verb, that translates a HTTP verb into a number, in fact an enum. The most common verbs are GET POST, PUT and DELETE, however in total there are 33 verbs, the longest ones have 12 characters.

Why the boost implementation seemed to me to be odd? It's basically a hardcoded Trie. There is the main switch statement that selects a subtree based on the first character. Then:

This looks like a lot of branches and we do know that branches might be a source of performance problems. A mispredicted branch penalty is several CPU cycles. I thought it would be good to check if other solutions would be better.

  1. The first solution is to match not a single letter prefix, but take into account four or eight characters at time. It costs exactly one load and comparison. The only drawback is padding with zeros strings shorter than 4 chars.
  2. Since the set of verbs is small and given statically, we may build a minimal perfect hash function (MPH). GNU gperf can be used to generate a C++ program implementing a MPH. The major drawback of gperf is that it generates function "exists", while we need a "lookup". I had to manually edit the generated program.

Experiment results

Experiment setup. We generate predefined number of random HTTP verbs. Verbs are either selected from the whole set of 33 verbs, or from subset GET/POST/PUT. Then a procedure is called given number of times and the total time is noted.

We tested five procedures:

  1. original boost::beast;
  2. SWAR64 — match string in 8-byte chunks;
  3. SWAR32 — match string in 4-byte chunks;
  4. SWAR32v2 — match string in 4-byte chunks, with a fast path for strings not longer than 4 bytes;
  5. MPH — minimal perfect hash function.

Summary:

Skylake

  • CPU: Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz
  • compiler: gcc version 8.4.0 (Ubuntu 8.4.0-1ubuntu1~16.04.1)
All HTTP verbs
procedure input size iterations time [us]
boost::beast 100 10000 2956
SWAR64 100 10000 6922
SWAR32 100 10000 7200
SWAR32v2 100 10000 7024
MPH 100 10000 5464
boost::beast 1000 1000 9315
SWAR64 1000 1000 7932
SWAR32 1000 1000 8834
SWAR32v2 1000 1000 9485
MPH 1000 1000 10352
boost::beast 10000 100 16457
SWAR64 10000 100 21809
SWAR32 10000 100 23204
SWAR32 10000 100 21766
MPH 10000 100 11089
GET/POST/PUT verbs
procedure input size iterations time [us]
boost::beast 100 10000 3334
SWAR64 100 10000 7321
SWAR32 100 10000 6502
SWAR32v2 100 10000 6421
MPH 100 10000 7891
boost::beast 1000 1000 5839
SWAR64 1000 1000 8820
SWAR32 1000 1000 8899
SWAR32v2 1000 1000 10027
MPH 1000 1000 7531
boost::beast 10000 100 8339
SWAR64 10000 100 14289
SWAR32 10000 100 13722
SWAR32v2 10000 100 12117
MPH 10000 100 7498

Skylake-X

  • CPU: Intel(R) Xeon(R) W-2104 CPU @ 3.20GHz
  • compiler: gcc version 8.4.0 (Ubuntu 8.4.0-1ubuntu1~16.04.1)
All HTTP verbs
procedure input size iterations time [us]
boost::beast 100 10000 9466
SWAR64 100 10000 16286
SWAR32 100 10000 9970
SWAR32v2 100 10000 8432
MPH 100 10000 6956
boost::beast 1000 1000 24203
SWAR64 1000 1000 13758
SWAR32 1000 1000 17132
SWAR32v2 1000 1000 11872
MPH 1000 1000 12606
boost::beast 10000 100 31449
SWAR64 10000 100 25088
SWAR32 10000 100 26211
SWAR32v2 10000 100 25921
MPH 10000 100 13683
GET/POST/PUT verbs
procedure input size iterations time [us]
boost::beast 100 10000 3579
SWAR64 100 10000 8458
SWAR32 100 10000 7687
SWAR32v2 100 10000 7713
MPH 100 10000 9542
boost::beast 1000 1000 6940
SWAR64 1000 1000 12876
SWAR32 1000 1000 11868
SWAR32v2 1000 1000 11431
MPH 1000 1000 9194
boost::beast 10000 100 10500
SWAR64 10000 100 16237
SWAR32 10000 100 15809
SWAR32v2 10000 100 14582
MPH 10000 100 9134

Source code

All implementations are available at github.

Implementations

Boost::beast approach

This is a verbose copy of boost::beast code.

verb
string_to_verb(std::string_view v)
{
    if(v.size() < 3)
        return verb::unknown;
    auto c = v[0];
    v.remove_prefix(1);
    switch(c)
    {
    case 'A':
        if(v == "CL"_sv)
            return verb::acl;
        break;

    case 'B':
        if(v == "IND"_sv)
            return verb::bind;
        break;

    case 'C':
        c = v[0];
        v.remove_prefix(1);
        switch(c)
        {
        case 'H':
            if(v == "ECKOUT"_sv)
                return verb::checkout;
            break;

        case 'O':
            if(v == "NNECT"_sv)
                return verb::connect;
            if(v == "PY"_sv)
                return verb::copy;
            ;

        default:
            break;
        }
        break;

    case 'D':
        if(v == "ELETE"_sv)
            return verb::delete_;
        break;

    case 'G':
        if(v == "ET"_sv)
            return verb::get;
        break;

    case 'H':
        if(v == "EAD"_sv)
            return verb::head;
        break;

    case 'L':
        if(v == "INK"_sv)
            return verb::link;
        if(v == "OCK"_sv)
            return verb::lock;
        break;

    case 'M':
        c = v[0];
        v.remove_prefix(1);
        switch(c)
        {
        case '-':
            if(v == "SEARCH"_sv)
                return verb::msearch;
            break;

        case 'E':
            if(v == "RGE"_sv)
                return verb::merge;
            break;

        case 'K':
            if(v == "ACTIVITY"_sv)
                return verb::mkactivity;
            if(v[0] == 'C')
            {
                v.remove_prefix(1);
                if(v == "ALENDAR"_sv)
                    return verb::mkcalendar;
                if(v == "OL"_sv)
                    return verb::mkcol;
                break;
            }
            break;

        case 'O':
            if(v == "VE"_sv)
                return verb::move;
            ;

        default:
            break;
        }
        break;

    case 'N':
        if(v == "OTIFY"_sv)
            return verb::notify;
        break;

    case 'O':
        if(v == "PTIONS"_sv)
            return verb::options;
        break;

    case 'P':
        c = v[0];
        v.remove_prefix(1);
        switch(c)
        {
        case 'A':
            if(v == "TCH"_sv)
                return verb::patch;
            break;

        case 'O':
            if(v == "ST"_sv)
                return verb::post;
            break;

        case 'R':
            if(v == "OPFIND"_sv)
                return verb::propfind;
            if(v == "OPPATCH"_sv)
                return verb::proppatch;
            break;

        case 'U':
            if(v == "RGE"_sv)
                return verb::purge;
            if(v == "T"_sv)
                return verb::put;
            ;

        default:
            break;
        }
        break;

    case 'R':
        if(v[0] != 'E')
            break;
        v.remove_prefix(1);
        if(v == "BIND"_sv)
            return verb::rebind;
        if(v == "PORT"_sv)
            return verb::report;
        break;

    case 'S':
        if(v == "EARCH"_sv)
            return verb::search;
        if(v == "UBSCRIBE"_sv)
            return verb::subscribe;
        break;

    case 'T':
        if(v == "RACE"_sv)
            return verb::trace;
        break;

    case 'U':
        if(v[0] != 'N')
            break;
        v.remove_prefix(1);
        if(v == "BIND"_sv)
            return verb::unbind;
        if(v == "LINK"_sv)
            return verb::unlink;
        if(v == "LOCK"_sv)
            return verb::unlock;
        if(v == "SUBSCRIBE"_sv)
            return verb::unsubscribe;
        break;

    default:
        break;
    }

    return verb::unknown;
}

Matching more characters at once

64-bit procedure

Below is a procedure that matches 8-byte prefixes.

#define STRING_CONST(s0, s1, s2, s3, s4, s5, s6, s7) \
   ((uint64_t(uint8_t((s0))) << 0*8) | \
    (uint64_t(uint8_t((s1))) << 1*8) | \
    (uint64_t(uint8_t((s2))) << 2*8) | \
    (uint64_t(uint8_t((s3))) << 3*8) | \
    (uint64_t(uint8_t((s4))) << 4*8) | \
    (uint64_t(uint8_t((s5))) << 5*8) | \
    (uint64_t(uint8_t((s6))) << 6*8) | \
    (uint64_t(uint8_t((s7))) << 7*8))

verb
string_to_verb(std::string_view v)
{
    if (v.size() < 3 or v.size() > 13)
        return verb::unknown;

    union {
        char buf[13];
        uint64_t value;
    };

    value = 0;
    memcpy(buf, v.data(), v.size());

    switch (value) {
        case STRING_CONST('A', 'C', 'L', 0, 0, 0, 0, 0):
            return verb::acl;
        case STRING_CONST('B', 'I', 'N', 'D', 0, 0, 0, 0):
            return verb::bind;
        case STRING_CONST('C', 'H', 'E', 'C', 'K', 'O', 'U', 'T'):
            return verb::checkout;
        case STRING_CONST('C', 'O', 'N', 'N', 'E', 'C', 'T', 0):
            return verb::connect;
        case STRING_CONST('C', 'O', 'P', 'Y', 0, 0, 0, 0):
            return verb::copy;
        case STRING_CONST('D', 'E', 'L', 'E', 'T', 'E', 0, 0):
            return verb::delete_;
        case STRING_CONST('G', 'E', 'T', 0, 0, 0, 0, 0):
            return verb::get;
        case STRING_CONST('H', 'E', 'A', 'D', 0, 0, 0, 0):
            return verb::head;
        case STRING_CONST('L', 'I', 'N', 'K', 0, 0, 0, 0):
            return verb::link;
        case STRING_CONST('L', 'O', 'C', 'K', 0, 0, 0, 0):
            return verb::lock;
        case STRING_CONST('M', '-', 'S', 'E', 'A', 'R', 'C', 'H'):
            return verb::msearch;
        case STRING_CONST('M', 'E', 'R', 'G', 'E', 0, 0, 0):
            return verb::merge;
        case STRING_CONST('M', 'K', 'A', 'C', 'T', 'I', 'V', 'I'):
            if (v.size() == 10 and v.substr(8) == "TY"_sv)
                return verb::mkactivity;
            break;
        case STRING_CONST('M', 'K', 'C', 'A', 'L', 'E', 'N', 'D'):
            if (v.size() == 10 and v.substr(8) == "AR"_sv)
                return verb::mkcalendar;
            break;
        case STRING_CONST('M', 'K', 'C', 'O', 'L', 0, 0, 0):
            return verb::mkcol;
        case STRING_CONST('M', 'O', 'V', 'E', 0, 0, 0, 0):
            return verb::move;
        case STRING_CONST('N', 'O', 'T', 'I', 'F', 'Y', 0, 0):
            return verb::notify;
        case STRING_CONST('O', 'P', 'T', 'I', 'O', 'N', 'S', 0):
            return verb::options;
        case STRING_CONST('P', 'A', 'T', 'C', 'H', 0, 0, 0):
            return verb::patch;
        case STRING_CONST('P', 'O', 'S', 'T', 0, 0, 0, 0):
            return verb::post;
        case STRING_CONST('P', 'R', 'O', 'P', 'F', 'I', 'N', 'D'):
            return verb::propfind;
        case STRING_CONST('P', 'R', 'O', 'P', 'P', 'A', 'T', 'C'):
            if (v.size() == 9 and v[8] == 'H')
                return verb::proppatch;
            break;
        case STRING_CONST('P', 'U', 'R', 'G', 'E', 0, 0, 0):
            return verb::purge;
        case STRING_CONST('P', 'U', 'T', 0, 0, 0, 0, 0):
            return verb::put;
        case STRING_CONST('R', 'E', 'B', 'I', 'N', 'D', 0, 0):
            return verb::rebind;
        case STRING_CONST('R', 'E', 'P', 'O', 'R', 'T', 0, 0):
            return verb::report;
        case STRING_CONST('S', 'E', 'A', 'R', 'C', 'H', 0, 0):
            return verb::search;
        case STRING_CONST('S', 'U', 'B', 'S', 'C', 'R', 'I', 'B'):
            if (v.size() == 9 and v[8] == 'E')
                return verb::subscribe;
            break;
        case STRING_CONST('T', 'R', 'A', 'C', 'E', 0, 0, 0):
            return verb::trace;
        case STRING_CONST('U', 'N', 'B', 'I', 'N', 'D', 0, 0):
            return verb::unbind;
        case STRING_CONST('U', 'N', 'L', 'I', 'N', 'K', 0, 0):
            return verb::unlink;
        case STRING_CONST('U', 'N', 'L', 'O', 'C', 'K', 0, 0):
            return verb::unlock;
        case STRING_CONST('U', 'N', 'S', 'U', 'B', 'S', 'C', 'R'):
            if (v.size() == 11 and v.substr(8) == "IBE"_sv)
                return verb::unsubscribe;
            break;
    }

    return verb::unknown;
}

32-bit procedure

Below is a procedure that matches 4-byte prefixes.

#define STRING_CONST(s0, s1, s2, s3) \
   ((uint64_t(uint8_t((s0))) << 0*8) | \
    (uint64_t(uint8_t((s1))) << 1*8) | \
    (uint64_t(uint8_t((s2))) << 2*8) | \
    (uint64_t(uint8_t((s3))) << 3*8))

verb
string_to_verb(std::string_view v)
{
    if (v.size() < 3 or v.size() > 13)
        return verb::unknown;

    union {
        char buf[13];
        uint32_t word[2];
        uint32_t value;
    };

    value = 0;
    memcpy(buf, v.data(), v.size());

    switch (value) {
        case STRING_CONST('A', 'C', 'L', 0):
            return verb::acl;
        case STRING_CONST('B', 'I', 'N', 'D'):
            return verb::bind;
        case STRING_CONST('C', 'H', 'E', 'C'):
            if (word[1] == STRING_CONST('K', 'O', 'U', 'T'))
                return verb::checkout;
            break;
        case STRING_CONST('C', 'O', 'N', 'N'):
            if (word[1] == STRING_CONST('E', 'C', 'T', 0))
                return verb::connect;
            break;
        case STRING_CONST('C', 'O', 'P', 'Y'):
            return verb::copy;
        case STRING_CONST('D', 'E', 'L', 'E'):
            if (word[1] == STRING_CONST('T', 'E', 0, 0))
                return verb::delete_;
            break;
        case STRING_CONST('G', 'E', 'T', 0):
            return verb::get;
        case STRING_CONST('H', 'E', 'A', 'D'):
            return verb::head;
        case STRING_CONST('L', 'I', 'N', 'K'):
            return verb::link;
        case STRING_CONST('L', 'O', 'C', 'K'):
            return verb::lock;
        case STRING_CONST('M', '-', 'S', 'E'):
            if (word[1] == STRING_CONST('A', 'R', 'C', 'H'))
                return verb::msearch;
            break;
        case STRING_CONST('M', 'E', 'R', 'G'):
            if (word[1] == STRING_CONST('E', 0, 0, 0))
                return verb::merge;
            break;
        case STRING_CONST('M', 'K', 'A', 'C'):
            if (v.substr(4) == /*MKAC*/"TIVITY"_sv)
                return verb::mkactivity;
            break;
        case STRING_CONST('M', 'K', 'C', 'A'):
            if (v.substr(4) == /*MKCA*/"LENDAR"_sv)
                return verb::mkcalendar;
            break;
        case STRING_CONST('M', 'K', 'C', 'O'):
            if (word[1] == STRING_CONST('L', 0, 0, 0))
                return verb::mkcol;
            break;
        case STRING_CONST('M', 'O', 'V', 'E'):
            return verb::move;
        case STRING_CONST('N', 'O', 'T', 'I'):
            if (word[1] == STRING_CONST('F', 'Y', 0, 0))
                return verb::notify;
            break;
        case STRING_CONST('O', 'P', 'T', 'I'):
            if (word[1] == STRING_CONST('O', 'N', 'S', 0))
                return verb::options;
            break;
        case STRING_CONST('P', 'A', 'T', 'C'):
            if (v[4] == 'H')
                return verb::patch;
            break;
        case STRING_CONST('P', 'O', 'S', 'T'):
            return verb::post;
        case STRING_CONST('P', 'R', 'O', 'P'):
            if (v.substr(4) == /*PROP*/"FIND"_sv)
                return verb::propfind;
            else if (v.substr(4) == /*PROP*/"PATCH"_sv)
                return verb::proppatch;
            break;
        case STRING_CONST('P', 'U', 'R', 'G'):
            if (v[4] == 'E')
                return verb::purge;
            break;
        case STRING_CONST('P', 'U', 'T', 0):
            return verb::put;
        case STRING_CONST('R', 'E', 'B', 'I'):
            if (word[1] == STRING_CONST('N', 'D', 0, 0))
                return verb::rebind;
            break;
        case STRING_CONST('R', 'E', 'P', 'O'):
            if (word[1] == STRING_CONST('R', 'T', 0, 0))
                return verb::report;
            break;
        case STRING_CONST('S', 'E', 'A', 'R'):
            if (word[1] == STRING_CONST('C', 'H', 0, 0))
                return verb::search;
            break;
        case STRING_CONST('S', 'U', 'B', 'S'):
            if (v.substr(4) == /*SUBS*/"CRIBE"_sv)
                return verb::subscribe;
            break;
        case STRING_CONST('T', 'R', 'A', 'C'):
            if (v[4] == 'E')
                return verb::trace;
            break;
        case STRING_CONST('U', 'N', 'B', 'I'):
            if (word[1] == STRING_CONST('N', 'D', 0, 0))
                return verb::unbind;
            break;
        case STRING_CONST('U', 'N', 'L', 'I'):
            if (word[1] == STRING_CONST('N', 'K', 0, 0))
                return verb::unlink;
            break;
        case STRING_CONST('U', 'N', 'L', 'O'):
            if (word[1] == STRING_CONST('C', 'K', 0, 0))
                return verb::unlock;
            break;
        case STRING_CONST('U', 'N', 'S', 'U'):
            if (v.substr(4) == /*UNSU*/"BSCRIBE"_sv)
                return verb::unsubscribe;
            break;
    }

    return verb::unknown;
}

Another 32-bit variant

32-bit procedure (variant #2)

This is a slightly modified 32-bit procedure. It separately matches strings shorts string (0..4 bytes) and longer ones.

verb
string_to_verb_v2(std::string_view v)
{
    if (v.size() < 3 or v.size() > 13)
        return verb::unknown;

    union {
        char buf[13];
        uint32_t word[2];
        uint32_t value;
    };

    value = 0;
    memcpy(buf, v.data(), v.size());

    if (v.size() <= 4)
    {
        switch (value) {
            case STRING_CONST('G', 'E', 'T', 0):
                return verb::get;
            case STRING_CONST('H', 'E', 'A', 'D'):
                return verb::head;
            case STRING_CONST('P', 'O', 'S', 'T'):
                return verb::post;
            case STRING_CONST('P', 'U', 'T', 0):
                return verb::put;
        }

        switch (value) {
            case STRING_CONST('A', 'C', 'L', 0):
                return verb::acl;
            case STRING_CONST('B', 'I', 'N', 'D'):
                return verb::bind;
            case STRING_CONST('C', 'O', 'P', 'Y'):
                return verb::copy;
            case STRING_CONST('L', 'I', 'N', 'K'):
                return verb::link;
            case STRING_CONST('L', 'O', 'C', 'K'):
                return verb::lock;
            case STRING_CONST('M', 'O', 'V', 'E'):
                return verb::move;
        }
    }

    switch (value) {
        case STRING_CONST('C', 'H', 'E', 'C'):
            if (word[1] == STRING_CONST('K', 'O', 'U', 'T'))
                return verb::checkout;
            break;
        case STRING_CONST('C', 'O', 'N', 'N'):
            if (word[1] == STRING_CONST('E', 'C', 'T', 0))
                return verb::connect;
            break;
        case STRING_CONST('D', 'E', 'L', 'E'):
            if (word[1] == STRING_CONST('T', 'E', 0, 0))
                return verb::delete_;
            break;
        case STRING_CONST('M', '-', 'S', 'E'):
            if (word[1] == STRING_CONST('A', 'R', 'C', 'H'))
                return verb::msearch;
            break;
        case STRING_CONST('M', 'E', 'R', 'G'):
            if (word[1] == STRING_CONST('E', 0, 0, 0))
                return verb::merge;
            break;
        case STRING_CONST('M', 'K', 'A', 'C'):
            if (v.substr(4) == /*MKAC*/"TIVITY"_sv)
                return verb::mkactivity;
            break;
        case STRING_CONST('M', 'K', 'C', 'A'):
            if (v.substr(4) == /*MKCA*/"LENDAR"_sv)
                return verb::mkcalendar;
            break;
        case STRING_CONST('M', 'K', 'C', 'O'):
            if (word[1] == STRING_CONST('L', 0, 0, 0))
                return verb::mkcol;
            break;
        case STRING_CONST('N', 'O', 'T', 'I'):
            if (word[1] == STRING_CONST('F', 'Y', 0, 0))
                return verb::notify;
            break;
        case STRING_CONST('O', 'P', 'T', 'I'):
            if (word[1] == STRING_CONST('O', 'N', 'S', 0))
                return verb::options;
            break;
        case STRING_CONST('P', 'A', 'T', 'C'):
            if (v[4] == 'H')
                return verb::patch;
            break;
        case STRING_CONST('P', 'R', 'O', 'P'):
            if (v.substr(4) == /*PROP*/"FIND"_sv)
                return verb::propfind;
            else if (v.substr(4) == /*PROP*/"PATCH"_sv)
                return verb::proppatch;
            break;
        case STRING_CONST('P', 'U', 'R', 'G'):
            if (v[4] == 'E')
                return verb::purge;
            break;
        case STRING_CONST('R', 'E', 'B', 'I'):
            if (word[1] == STRING_CONST('N', 'D', 0, 0))
                return verb::rebind;
            break;
        case STRING_CONST('R', 'E', 'P', 'O'):
            if (word[1] == STRING_CONST('R', 'T', 0, 0))
                return verb::report;
            break;
        case STRING_CONST('S', 'E', 'A', 'R'):
            if (word[1] == STRING_CONST('C', 'H', 0, 0))
                return verb::search;
            break;
        case STRING_CONST('S', 'U', 'B', 'S'):
            if (v.substr(4) == /*SUBS*/"CRIBE"_sv)
                return verb::subscribe;
            break;
        case STRING_CONST('T', 'R', 'A', 'C'):
            if (v[4] == 'E')
                return verb::trace;
            break;
        case STRING_CONST('U', 'N', 'B', 'I'):
            if (word[1] == STRING_CONST('N', 'D', 0, 0))
                return verb::unbind;
            break;
        case STRING_CONST('U', 'N', 'L', 'I'):
            if (word[1] == STRING_CONST('N', 'K', 0, 0))
                return verb::unlink;
            break;
        case STRING_CONST('U', 'N', 'L', 'O'):
            if (word[1] == STRING_CONST('C', 'K', 0, 0))
                return verb::unlock;
            break;
        case STRING_CONST('U', 'N', 'S', 'U'):
            if (v.substr(4) == /*UNSU*/"BSCRIBE"_sv)
                return verb::unsubscribe;
            break;
    }

    return verb::unknown;
}

Minimal perfect hashing function

Below is MPH implementation manually adjusted to return the value assigned to given key, not only the information whether key is valid or not.

inline unsigned int
perfect_hash::hash(std::string_view str)
{
  static const unsigned char asso_values[] =
    {
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 10,  5, 15,  0,  5,
      25,  0, 10, 70, 15, 70,  0,  0, 20,  0,
       5, 25, 20,  0,  0,  0, 40, 70, 70, 70,
      20, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70, 70, 70, 70,
      70, 70, 70, 70, 70, 70, 70
    };
  auto hval = str.size();

  switch (hval)
    {
      default:
        hval += asso_values[static_cast<unsigned char>(str[3]+1)];
      /*FALLTHROUGH*/
      case 3:
        hval += asso_values[static_cast<unsigned char>(str[2])];
      /*FALLTHROUGH*/
      case 2:
      case 1:
        hval += asso_values[static_cast<unsigned char>(str[0])];
        break;
    }
  return hval;
}


perfect_hash::verb
perfect_hash::string_to_verb(std::string_view str)
{
  enum
    {
      TOTAL_KEYWORDS = 33,
      MIN_WORD_LENGTH = 3,
      MAX_WORD_LENGTH = 11,
      MIN_HASH_VALUE = 3,
      MAX_HASH_VALUE = 69
    };

  static std::string_view wordlist[] =
    {
      "", "", "",
      "GET",
      "", "", "", "",
      "PUT",
      "POST",
      "PATCH",
      "UNLOCK",
      "",
      "ACL",
      "SUBSCRIBE",
      "TRACE",
      "SEARCH",
      "", "",
      "LOCK",
      "MKACTIVITY",
      "UNLINK",
      "OPTIONS",
      "",
      "LINK",
      "MKCOL",
      "UNBIND",
      "",
      "CHECKOUT",
      "HEAD",
      "MKCALENDAR",
      "DELETE",
      "",
      "M-SEARCH",
      "BIND",
      "MERGE",
      "REPORT",
      "",
      "PROPFIND",
      "PROPPATCH",
      "PURGE",
      "NOTIFY",
      "CONNECT",
      "",
      "COPY",
      "",
      "REBIND",
      "", "", "", "",
      "UNSUBSCRIBE",
      "", "", "", "", "", "", "", "", "",
      "", "", "", "", "", "", "", "",
      "MOVE"
    };

  static verb values[] =
    {
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::get,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::put,
      verb::post,
      verb::patch,
      verb::unlock,
      verb::unknown,
      verb::acl,
      verb::subscribe,
      verb::trace,
      verb::search,
      verb::unknown,
      verb::unknown,
      verb::lock,
      verb::mkactivity,
      verb::unlink,
      verb::options,
      verb::unknown,
      verb::link,
      verb::mkcol,
      verb::unbind,
      verb::unknown,
      verb::checkout,
      verb::head,
      verb::mkcalendar,
      verb::delete_,
      verb::unknown,
      verb::msearch,
      verb::bind,
      verb::merge,
      verb::report,
      verb::unknown,
      verb::propfind,
      verb::proppatch,
      verb::purge,
      verb::notify,
      verb::connect,
      verb::unknown,
      verb::copy,
      verb::unknown,
      verb::rebind,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unsubscribe,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::unknown,
      verb::move
    };

  if (str.size() <= MAX_WORD_LENGTH && str.size() >= MIN_WORD_LENGTH)
    {
      const unsigned int key = hash(str);

      if (key <= MAX_HASH_VALUE) {
          if (wordlist[key] == str)
            return values[key];
      }
    }
  return verb::unknown;
}