Jump to content

dcgreen2k

Member
  • Posts

    1,947
  • Joined

  • Last visited

Everything posted by dcgreen2k

  1. It's fixed when compiling without optimizations, but the function calls are still getting optimized out with -O1 and up. Take a look at labels .L12 (strlen), .L13 (nstrlen), and .L14 (mystrlen) here: https://godbolt.org/z/bPhbK74Tc The builtin strlen test gets precomputed by the compiler and the result of mystrlen just gets multiplied by 1000000. It's kind of funny how hard the compiler tries to optimize these kinds of things. This is closer to what I expected from the tests. The builtin strlen is super fast due to using AVX2, nstrlen is slower with its simple iterator-based approach, and mystrlen uses a simple index-based approach. Are you running our versions of the testing code, where the calls to strlen are inside a loop? You won't get accurate results if you just run the functions once.
  2. I modified the testing code with some inline assembly so that the builtin strlen function would actually get called. The assembly is designed to match the compiler output for the other loops, and the code should be compiled with optimizations off. Otherwise, it'll segfault. Its assembly can be seen here: https://godbolt.org/z/zzTE4o6Ez Running the new tests, we get this. Again, these results are with optimizations turned off. Even after ensuring we call the function directly, the builtin strlen test runs incredibly quickly. What's going on? To figure this out, I stepped through the testing code in GDB until I got to the strlen call. Stepping into strlen, GDB reported this: Looking online, the source is available here: https://github.com/bminor/glibc/blob/master/sysdeps/x86_64/multiarch/strlen-avx2.S So the strlen function that's getting called isn't the simple version we found earlier, it's all handwritten AVX2 assembly. At this point, I'm looking at the wall of assembly code and the elapsed time for the builtin strlen test and I don't really believe it. So I decided to step into __strlen_avx2 in GDB and manually count how many times I have to hit enter before it returns. For a 2048-byte string, only 200 instructions were executed with 16 iterations of its loop. Comparing the AVX2 version's 0.023636s with 0.519933s (with -O1) for the kernel strlen we found earlier, there's a 22x speedup. Knowing that the strlen we found earlier iterates over every character and executes 3 instructions per loop, these seem like reasonable results. ----------------- Back to the original question, the "regular" builtin version of strlen is in fact slightly faster than your own version despite it appearing more complex. This is due to it being iterator-based compared to your index-based approach. With the index-based approach, the program needs to add the index to the array's base address, then dereference the resulting memory address to get the current character. In contrast, the iterator-based approach only needs to dereference the pointer it hangs onto. I will stress that this difference is a serious micro-optimization and saves only a single assembly instruction when optimizations are turned on. There are better ways to make your program faster.
  3. I had a look at the generated assembly, and the compiler didn't inline any of the functions. The relevant function calls just aren't there, because the compiler knows we aren't doing anything with the return values. Take a look at the instructions immediately after labels .L12, .L13, .L14, and .L15 here: https://godbolt.org/z/4f156drWK .L15 corresponds to the empty loop and only subtracts 1 from the loop counter before jumping back up. .L12 and .L14 correspond to the builtin strlen and mystrlen tests and show the same instructions as the empty loop. .L13 is the loop for the kernelstrlen test and is the only one to keep the function call. I'm not sure why this is the case.
  4. @Eigenvektor @tikker just wanted to let you know that the call to (built-in) strlen in your testing code gets optimized away, even when compiling without optimizations. When compiling with -O1, all functions get optimized out except for the kernelstrlen function, strangely. No optimizations: -O1: The same thing happens on GCC 13.2, which is the latest version on Godbolt. My testing code:
  5. One resource that helped me out a lot when I was interested in this kind of thing is the OSDev Wiki. It has lots of great guides and info about writing a kernel and incorporating it into an operating system. https://wiki.osdev.org/Expanded_Main_Page This is a good tutorial to get started. It lists the tools you need and shows how to use assembly and C for a kernel that prints Hello World. https://wiki.osdev.org/Bare_Bones
  6. Including headers in C does not slow down the program. It can only slow down compilation, but this isn't something you should be worried about. Including a header and importing a module let you do similar things but are very different under the hood. In C and C++, including a header is very simple - the compiler copies and pastes the contents of the header file into your source code file where the #include statement is. Python's import statement is much more complex and I'm not that familiar with the specifics of it. However, imports are done at runtime since Python code isn't compiled in the same way C code is. In short: C's #include: May slow down compilation, no effect on runtime speed Python's import: May slow down runtime Neither of these are things you should worry about, though
  7. In C, you cannot use == to check for string equality. This is because the == operator checks if the strings' pointers are equal, rather than checking the characters they contain. To fix this, use the strcmp function. strcmp will return 0 if the two strings are equal. For example, this code: if(Input_RPS == "q"){ printf("Sorry to see you go"); break; } would be correctly written as: if(strcmp(Input_RPS, "q") == 0){ printf("Sorry to see you go"); break; }
  8. Both. When I'm with people who have experience programming or work with computers professionally, like those in my university classes or in my cybersecurity research group, we usually pronounce it gooey. When I'm around people who don't, we always say the initials.
  9. Eigenvektor did a great job explaining how to use srand, but I can explain a bit more about strings in C. In C, a string is just an array of chars ending with a null byte. We can construct a string from scratch like this: #include <stdio.h> int main() { // Allocate space to hold the string char string[10]; // Place characters in the array string[0] = 'H'; string[1] = 'e'; string[2] = 'l'; string[3] = 'l'; string[4] = 'o'; // Don't forget the null byte string[5] = '\0'; // Print the string to the terminal puts(string); return 0; } This prints out Hello to the terminal. Note that even though our array has enough space to store 10 chars, only 5 get printed out. This is because functions operating on strings, like puts and strlen, stop when they encounter a null byte. That's the '\0' in the code above. If your string doesn't end with a null byte, then string functions can run further than they should and access invalid memory. That being said, you most likely won't work with strings in this way very often. It's more convenient to use string literals like what you did in your original post. All three of these are valid ways of storing a string using string literals: // String literals are automatically null-terminated char s1[6] = "Hello"; char s2[] = "Hello"; char* s3 = "Hello"; The difference between the first two methods and the third is that the third is read-only. This is because the first two strings are allocated on the stack while the third is allocated in a read-only section of the program. For example, we're able to modify the first two strings no problem: #include <stdio.h> int main() { // String literals are automatically null-terminated char s1[6] = "Hello"; char s2[] = "Hello"; char* s3 = "Hello"; // Modify the first two strings s1[1] = 'a'; s2[0] = 'Y'; // Print the strings to the terminal puts(s1); puts(s2); puts(s3); return 0; } But trying to modify the third string crashes the program with a segmentation fault, because we tried writing to read-only memory. #include <stdio.h> int main() { // String literals are automatically null-terminated char s1[6] = "Hello"; char s2[] = "Hello"; char* s3 = "Hello"; // Try to modify the third string s3[1] = 'a'; // Print the strings to the terminal puts(s1); puts(s2); puts(s3); return 0; } The advantage of using string literals is that you can let the compiler decide how to store the string, so you don't have to worry about the size of the array or forgetting the null terminator. Lastly, here's how I would write the code you initially posted:
  10. Newegg usually has good deals on Rosewill products, since that's their in-house brand. Other than that, eBay has also been a pretty good option.
  11. A few months ago I bought a Rosewill Neon K91 keyboard with Kailh Brown switches for around 25 USD, been very happy with it so far and it feels great to type on. I also got a Redragon Kumara for 40 USD about 8 years ago and it's still going strong.
  12. The error is saying that you didn't declare the variable i before using it. In the code shown in the error message, you're assigning a value to i but the compiler doesn't know what to do with it because you didn't give i a type (short, int, long, etc.). You can fix the error by replacing i = 0 with int i = 0.
  13. You're right, it's been a while since I last worked with byte-level pointer arithmetic. You're supposed to use something like uint8_t* or char* instead of void* when doing pointer arithmetic like I showed previously. This is because void is an incomplete type and has no size, and GCC only allows us to use void* here because of an extension (i.e, it's non-standard). Compiling with -Wpedantic would have caught this. I've corrected the code I posted previously, in case someone wants to use it in the future.
  14. Your point about considering the size of the pointer instead of the string is correct. I wouldn't use sizeof() to check if you have enough space, because that should be handled by the length and capacity variables stored in the ArrayList struct. If you're adding something like an int, then you're adding the actual variable to the ArrayList. You'll typically add the pointer instead when working with arrays (which strings are, at the base level) and large structs. I'm not really sure what you mean by this. Here are a few simplified ArrayList functions I wrote based on your code. In my code, I made certain that the ArrayList is storing the actual variables instead of just pointers (where applicable). The add and get functions still use void* because we want to say "this can be any type" in C. Doing this requires a bit of pointer arithmetic and you'll notice that the backing array for the ArrayList struct is now just a uint8_t* (not void*, since void pointer arithmetic is non-standard) instead of void**. In my testing code, I show how this ArrayList can store ints or strings. Lastly, the only place sizeof() is ever used is during the call to ArrayList_init, and strlen() is never used. ArrayList.h #ifndef ARRAYLIST_H #define ARRAYLIST_H #include <stddef.h> #include <stdint.h> typedef struct ArrayList { uint8_t* array; size_t length, capacity, elemSize; // Added elemSize, removed filled_mem } ArrayList; // By passing in the size of an individual element, we can store the actual // elements inside the ArrayList instead of just pointers void ArrayList_init(ArrayList* list, size_t capacity, size_t elemSize); void ArrayList_add(ArrayList* list, const void* element); void* ArrayList_get(ArrayList* list, size_t index); void ArrayList_remove(ArrayList* list, size_t index); #endif ArrayList.c #include <malloc.h> #include <stdlib.h> #include <string.h> #include "ArrayList.h" void ArrayList_init(ArrayList* list, size_t capacity, size_t elemSize) { // Capacity must be at least 1 if (capacity < 1) { fprintf(stderr, "Capacity must be at least 1"); exit(1); } // We're going to be storing the actual elements, not just pointers list->array = malloc(capacity * elemSize); // Make sure the allocation didn't fail if (list->array == NULL) { fprintf(stderr, "Memory allocation failed"); exit(1); } list->length = 0; list->capacity = capacity; list->elemSize = elemSize; } void ArrayList_add(ArrayList* list, const void* const element) { // If the list is full, double its capacity if (list->length == list->capacity) { list->array = realloc(list->array, list->capacity * list->elemSize * 2); list->capacity *= 2; } // Now add the new element memcpy(list->array + list->length * list->elemSize, element, list->elemSize); list->length++; } void* ArrayList_get(ArrayList* list, size_t index) { // Check if the index is valid if (index >= list->length) { fprintf(stderr, "Index out of bounds"); exit(1); } // Return the address of the element associated with that index return list->array + (index * list->elemSize); } void ArrayList_remove(ArrayList* list, size_t index) { // Got too tired sorry // I'll probably do this tomorrow } main.c #include <stdio.h> #include "ArrayList.h" int main() { // Basic testing code // Let's start with a list of ints ArrayList intList; ArrayList_init(&intList, 100, sizeof(int)); // Add some ints int i1 = 123; int i2 = 456; ArrayList_add(&intList, (void*) &i1); ArrayList_add(&intList, (void*) &i2); ArrayList_add(&intList, (void*) &i2); // Retrieve those ints printf("%d\n", *((int*) ArrayList_get(&intList, 0))); printf("%d\n", *((int*) ArrayList_get(&intList, 1))); printf("%d\n", *((int*) ArrayList_get(&intList, 2))); // How about a list of strings now? ArrayList strList; ArrayList_init(&strList, 100, sizeof(char*)); // Add some strings char* s1 = "blah blah blah"; char* s2 = "C go brrrrrr"; ArrayList_add(&strList, (void*) &s1); ArrayList_add(&strList, (void*) &s2); ArrayList_add(&strList, (void*) &s2); // Retrieve those strings printf("%s\n", *((char**) ArrayList_get(&strList, 0))); printf("%s\n", *((char**) ArrayList_get(&strList, 1))); printf("%s\n", *((char**) ArrayList_get(&strList, 2))); return 0; } I hope this code clears up some confusion, just wanted to show how you could simplify the ArrayList code and get the same results.
  15. I don't think we're on the same page here. You don't need to know the length of the string to add it to your ArrayList. Just add the string's pointer to the list and you're done, nothing else to do. Here's a simplified version of how adding strings to the backing array would work: #include <malloc.h> int main() { char* s1 = "blah blah blah"; char* s2 = "This is waaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaay longer than 16 bytes"; // The backing array for your ArrayList // Let's say it has a capacity of 2 elements, for a total of 16 bytes (2 * sizeof(void*) == 16) void** data = malloc(2); // Next, add the strings. Notice how we don't need the length of the strings to do this data[0] = (void*) s1; data[1] = (void*) s2; // And now we retrieve the strings printf("%s\n", (char*) data[0]); printf("%s\n", (char*) data[1]); return 0; } This prints out: No sizeof() or strlen() needed.
  16. If you're talking about knowing that from inside your ArrayList functions, you don't. The ArrayList doesn't know anything about the type of the data it stores, only the size of an individual element if we consider the ArrayList_init() change I mentioned previously. What are you trying to do that requires you to know the length of the string?
  17. That's exactly it. You only need to store the pointer to the first character in the string. sizeof() doesn't work on strings because it returns the size of the data type, which is char*. #include <stdio.h> int main() { char *hello = "Hello"; printf("%zu\n", sizeof(hello)); char *wait = "Wait a minute what's going on here"; printf("%zu\n", sizeof(wait)); return 0; } This prints: Instead, you need to use strlen() when working with strings. #include <stdio.h> #include <string.h> int main() { char *hello = "Hello"; printf("%zu\n", strlen(hello)); char *wait = "Wait a minute what's going on here"; printf("%zu\n", strlen(wait)); return 0; } This prints: strlen() works by iterating over each character in the string, incrementing a counter until it reaches the end. The end of a C string will always be a null byte, or hexadecimal 00.
  18. In an array, all elements must have the same size. A C string is just an array of chars with a null byte at the end, accessed using a pointer to its first element. If you have a string, then the memory to hold its chars must already be allocated. All you need to do to put a string in your ArrayList is add the pointer to its first element. Since all pointers are 8 bytes on a 64-bit machine, the element size is constant. In other words, you wouldn't be storing entire char arrays, only the char*. Here's how your ArrayList would store strings: #include <stdio.h> #include "ArrayList.h" int main() { ArrayList a; ArrayList_init(&a, 100); char *hello = "Hello "; char *world = "World!\n"; ArrayList_add(&a, (void *) hello); ArrayList_add(&a, (void *) world); printf("%s", (char *) ArrayList_get(&a, 0)); printf("%s", (char *) ArrayList_get(&a, 1)); return 0; } And here's what the code outputs:
  19. First, there was a bit of work to do before your code would build and run properly. The attributes applied to the function declarations were one source of the issues: The pure and malloc attributes only work on functions that return a value (i.e, not void) The noreturn attribute should only be used on functions that contain an infinite loop Also, some function declarations have incorrect return types: ArrayList_get should return a void* instead of void ArrayList_length should return size_t instead of void One thing I noticed but doesn't cause a compiler warning is that some functions don't have list size or bounds checking, like ArrayList_get and ArrayList_pop. Those will cause a runtime error when you try to get an out of bounds index or call pop on an empty list. After getting the code running, I tested the init, add, remove, get, and length functions and they seemed to work fine with operations that avoided error cases. About your question relating to getting the size of an element, there's no way to do that using only void pointers like your ArrayList does now. This is because casting a pointer to void* discards any size information related to the original data. Here's how we can test what happens when you try to dereference a void* and use sizeof on the result: #include <stdio.h> #include <stddef.h> size_t getsize(void* data) { return sizeof(data); } size_t getsize_deref(void* data) { return sizeof(*data); } int main() { int a = 123; printf("%zu\n", getsize((void*)&a)); printf("%zu\n", getsize_deref((void*)&a)); return 0; } And this prints out: Why does this happen? Put simply, a void* points to a single byte in memory and there's no way to go back to the original data type unless the programmer remembers what type it was. The solution to this problem is to take the size of the data type when you first initialize the list and store it inside the ArrayList struct. Then, whenever you need to know the size of an individual element inside the list, you just use that stored value. Here's what the new init function might look like: void ArrayList_init(ArrayList* restrict list, size_t capacity, size_t elemSize); And here's how you might use it to store a list of ints: #include "ArrayList.h" int main() { ArrayList intList; ArrayList_init(&intList, 100, sizeof(int)); return 0; } Lastly, there's no good way to get around using void pointers in cases like this when using C. That's just how C works. There's a good reason newer languages have support for templates and generics, so programmers don't have to write error-prone code like this.
  20. The usual way to remove an element from an array-backed list is to just shift all of the elements that come after it back 1 space. There's no need to use realloc here. The second version seems very complex for such a simple removal task. As for your question about having variables only be modifiable inside a single source file, I don't think that's possible in C. Those are called private variables, and C doesn't have them. In cases like this where you want a variable to be private but your programming language doesn't support it (like C and Python), it's common to prefix the variable or function name with an underscore. This doesn't actually prevent you from accessing it, but it signals to yourself and other developers, "hey please don't use this variable/function directly."
  21. They don't work because those are Linux commands. Do you have Windows Subsystem for Linux installed, like I mentioned previously?
  22. Using void pointers is the standard way to implement generics in C. If you really don't want to use void pointers, then you could use macros like shown here: https://stackoverflow.com/questions/16522341/pseudo-generics-in-c However, this generally isn't recommended. C++ has support for generics built in and will be much less of a headache to use.
  23. Looks like the next step is to start learning how to use Linux then. Jokes aside, there's little native support for C and C++ development on Windows outside of Visual Studio (not VSCode). There's a reason I keep recommending it. Other than that, it might help to learn how to program in a language that's less dependent on your operating system, like Java or Python. My opinion is that, yes, programming is generally much easier on Linux. But it's also highly dependent on what you're trying to do. I work in cybersecurity research and mainly use Python for my job. My entire team uses Linux because it's easier to work with. However, I've also had cases where Windows was easier to use, like when I programmed in Java with a specific software stack and in C# with Unity. Cases like this are one reason "dual booting" is a popular option.
  24. Inside the parent folder for both of those libraries, there's a text file called INSTALL that shows the commands you need to run. The main commands will be: ./configure make make install
  25. Correct, CMake will not work with regular Makefiles.
×