C собеседование - кастинг и сравнение

я столкнулся с сложным (ИМО) вопросом. Мне нужно было сравнить два MAC-адресов, самым эффективным образом.

for цикл и сравнение местоположений, и я так и сделал, но интервьюер стремился к кастингу.

определение MAC:

typedef struct macA {
   char data[6];
} MAC;

и функция (та, которую меня попросили реализовать):

int isEqual(MAC* addr1, MAC* addr2)
{
    int i;

    for(i = 0; i<6; i++)
    {
        if(addr1->data[i] != addr2->data[i]) 
            return 0;
    }
    return 1;
}

но как упоминалось, что он стремился к кастингу.

значение, чтобы каким-то образом привести MAC-адрес, заданный int, сравнить оба адреса и вернуться.

но при забросе, int int_addr1 = (int)addr1;, только четыре байта будут отлиты, верно? Проверить остальные? То есть места 4 и 5?

и char и int являются целочисленными типами, поэтому кастинг является законным, но что происходит в описанной ситуации?

10 ответов


int isEqual(MAC* addr1, MAC* addr2)
{
    return memcmp(&addr1->data, &addr2->data, sizeof(addr1->data)) == 0;
}

Если ваш интервьюер требует, чтобы вы произвели неопределенное поведение, я, вероятно, буду искать работу в другом месте.

правильным начальным подходом было бы сохранить MAC-адрес в чем-то вроде uint64_t по крайней мере в памяти. Тогда сравнения были бы тривиальными и осуществимыми эффективно.


Ковбой время:

typedef struct macA {
   char data[6];
} MAC;

typedef struct sometimes_works {
   long some;
   short more;
} cast_it

typedef union cowboy
{
    MAC real;
    cast_it hack;
} cowboy_cast;

int isEqual(MAC* addr1, MAC* addr2)
{
     assert(sizeof(MAC) == sizeof(cowboy_cast));  // Can't be bigger
     assert(sizeof(MAC) == sizeof(cast_it));      // Can't be smaller

     if ( ( ((cowboy_cast *)addr1)->hack.some == ((cowboy_cast *)addr2)->hack.some )
       && ( ((cowboy_cast *)addr1)->hack.more == ((cowboy_cast *)addr2)->hack.more ) )
             return (0 == 0);

     return (0 == 42);
}

нет ничего плохого в эффективной реализации, для всех вы знаете, это было определено как горячий код, который вызывается много-много раз. И в любом случае, это нормально для вопросов интервью, чтобы иметь нечетные ограничения.

логично и априори является разветвленной инструкцией из-за оценки короткого замыкания, даже если она не компилируется таким образом, поэтому позволяет избежать этого, нам это не нужно. Также нам не нужно преобразовывать наше возвращаемое значение в true bool (правда или false, а не 0 или все, что не ноль).

вот быстрое решение на 32-битном: XOR будет фиксировать различия или записывать различия в обеих частях и не будет отрицать условие на равные, а не неравные. LHS и RHS являются независимыми вычислениями, поэтому суперскалярный процессор может делать это параллельно.

int isEqual(MAC* addr1, MAC* addr2)
{
    return ~((*(int*)addr2 ^ *(int*)addr1) | (int)(((short*)addr2)[2] ^ ((short*)addr1)[2]));
}

редактировать
Целью вышеуказанного кода было показать, что это может быть сделано эффективно без ветвления. Комментарии указали, что этот C++ классифицирует это как неопределенное поведение. Хотя это правда, VS справляется с этим отлично. Без изменения определения структуры и сигнатуры функции интервьюера во избежание неопределенного поведения необходимо сделать дополнительную копию. Таким образом, неопределенный способ поведения без ветвления, но с дополнительной копией будет следующим:

int isEqual(MAC* addr1, MAC* addr2)
{
    struct IntShort
    {
        int   i;
        short s;
    };

    union MACU
    {
        MAC addr;
        IntShort is;
    };

    MACU u1;
    MACU u2;

    u1.addr = *addr1; // extra copy
    u2.addr = *addr2; // extra copy

    return ~((u1.is.i ^ u2.is.i) | (int)(u1.is.s ^ u2.is.s)); // still no branching
}

Это будет работать на большинстве систем,и быть быстрее, чем ваше решение.

int isEqual(MAC* addr1, MAC* addr2)
{
    return ((int32*)addr1)[0] == ((int32*)addr2)[0] &&  ((int16*)addr1)[2] == ((int16*)addr2)[2];
}

было бы встроено красиво тоже, может быть удобно в центре цикла в системе, где вы можете проверить детали жизнеспособны.


не портативное решение, литье.

в платформе, которую я использую (на основе PIC24), есть тип int48, поэтому делаем безопасное предположение char 8 бит и обычные требования к выравниванию:

int isEqual(MAC* addr1, MAC* addr2) {
  return *((int48_t*) &addr1->data) == *((int48_t*) &addr2->data);
}

конечно, это не может быть использовано на многих платформах, но тогда есть ряд решений, которые также не являются портативными, в зависимости от предполагаемого int в размере no padding, etc.

самое высокое портативное решение (и разумно быстрое, учитывая хорошее компилятор) является memcmp() предлагается @H2CO3.

переход на более высокий уровень дизайна и использование достаточно широкого целочисленного типа, такого как uint64_t вместо struct macA, как предложил Kerrek SB, очень привлекателен.


у вас есть структура MAC (которая содержит массив из 6 байтов),

typedef struct {
    char data[6];
} MAC;

что согласуется с этой статьей о typedef для массива байтов фиксированной длины.

наивным подходом было бы предположить, что MAC-адрес выровнен по словам (что, вероятно, и хотел интервьюер), хотя и не гарантируется.

typedef unsigned long u32;
typedef   signed long s32;
typedef unsigned short u16;
typedef   signed short s16;

int
MACcmp(MAC* mac1, MAC* mac2)
{
    if(!mac1 || !mac2) return(-1); //check for NULL args
    u32 m1 = *(u32*)mac1->data;
    U32 m2 = *(u32*)mac2->data;
    if( m1 != m2 ) return (s32)m1 - (s32)m2;
    u16 m3 = *(u16*)(mac1->data+4);
    u16 m2 = *(u16*)(mac2->data+4);
    return (s16)m3 - (s16)m4;
}

немного безопаснее было бы интерпретировать char[6] как короткий[3] (MAC, скорее всего, будет выровнен по четным границам байтов, чем odd),

typedef unsigned short u16;
typedef   signed short s16;

int
MACcmp(MAC* mac1, MAC* mac2)
{
    if(!mac1 || !mac2) return(-1); //check for NULL args
    u16* p1 = (u16*)mac1->data;
    u16* p2 = (u16*)mac2->data;
    for( n=0; n<3; ++n ) {
        if( *p1 != *p2 ) return (s16)*p1 - (s16)*p2;
    }
    return(0);
}

ничего не предполагайте и скопируйте в Word выровненное хранилище, но единственная причина для типизации здесь-удовлетворить интервьюера,

typedef unsigned short u16;
typedef   signed short s16;

int
MACcmp(MAC* mac1, MAC* mac2)
{
    if(!mac1 || !mac2) return(-1); //check for NULL args
    u16 m1[3]; u16 p2[3];
    memcpy(m1,mac1->data,6);
    memcpy(m2,mac2->data,6);
    for( n=0; n<3; ++n ) {
        if( m1[n] != m2[n] ) return (s16)m1[n] - (s16)m2[n];
    }
    return(0);
}

сэкономьте себе много работы,

int
MACcmp(MAC* mac1, MAC* mac2)
{
    if(!mac1 || !mac2) return(-1);
    return memcmp(mac1->data,mac2->data,6);
}

функция memcmp в конечном итоге сделает сам цикл. Поэтому, используя его, вы в основном просто сделаете вещи менее эффективными (из-за дополнительного вызова функции).

вот необязательное решение:

typedef struct
{
    int x;
    short y;
}
MacAddr;

int isEqual(MAC* addr1, MAC* addr2)
{
    return *(MacAddr*)addr1 == *(MacAddr*)addr2;
}

компилятор, скорее всего, преобразует этот код в два сравнения, так как структура MacAddr содержит два поля.

полость: если ваш процессор не поддерживает несогласованные операции загрузки / хранения, addr1 и addr2 должны быть выровнены до 4 байтов (т. е., они должны быть расположены по адресам, которые делятся на 4). В противном случае при выполнении функции, скорее всего, произойдет нарушение доступа к памяти.

вы можете разделить структуру на 3 поля по 2 байта каждое или 6 полей по 1 байт каждое (уменьшая ограничение выравнивания до 2 или 1 соответственно). Но имейте в виду, что одно сравнение в исходном коде не обязательно является одним сравнением в исполняемом образе (т. е. во время выполнения).

BTW, несогласованный операции загрузки / хранения сами по себе могут добавить задержку выполнения, если им требуется больше "nops" в конвейере ЦП. Это действительно вопрос архитектуры процессора, в которой я сомневаюсь, что они хотели "копать" так далеко в вашем собеседовании. Однако, чтобы утверждать, что скомпилированный код не содержит таких операций (если они действительно "дороги"), вы можете гарантировать, что переменные всегда выровнены по 8 байтам и добавьте #pragma (директиву компилятора), сообщающую компилятору " не беспокоиться об этом."


возможно, он имел в виду определение MAC, которое использовало unsigned char и думал:

int isEqual(MAC* addr1, MAC* addr2) { return strncmp((*addr1).data,(*addr2).data,6)==0; }

что подразумевает приведение от (unsigned char *) к (char*). Во всяком случае, плохой вопрос.


чтобы сделать тип каламбура правильно, вы должны использовать объединение. В противном случае вы нарушите правила строгого сглаживания, которым следуют определенные компиляторы, и результат будет неопределенным.

int EqualMac( MAC* a , MAC* b )
{
    union
    {
        MAC m ;
        uint16_t w[3] ;

    } ua , ub ;

    ua.m = *a ;
    ub.m = *b ;

    if( ua.w[0] != ub.w[0] )  
        return 0 ;

    if( ua.w[1] != ub.w[1] )
        return 0 ;

    if( ua.w[2] != ub.w[2] )
        return 0 ;

return 1 ;
}

в соответствии с C99 безопасно читать из члена Союза, который не является последним, используемым для хранения значения в нем.

если элемент, используемый для чтения содержимого объекта объединения, не совпадает с элементом, последним используемым для хранения значения в объекте, соответствующая часть представление объекта значения интерпретируется как представление объекта в новом типе, как описано в 6.2.6 (процесс, иногда называемый "каламбуром типа"). Это может быть представление ловушки.