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 (процесс, иногда называемый "каламбуром типа"). Это может быть представление ловушки.