February 26, 2019
For today:
1) Start Homework 4
2) Read Think OS Chapter 5 and do the reading quiz
3) Prepare for a quiz (here's last year's quiz for practice)
Today:
1) Quiz
2) Homework 4 debrief
3) Homework 5 intro
4) Passing and returning arrays
For next time:
1) Read the sections below on bitwise operators and unit testing
2) Read Head First C Chapter 5
3) Finish Homework 4
4) Work on your project
C is only semi-portable
Hence preprocessor conditionals
Why separate compilation?
Why not separate compilation?
1) Open ExercisesInC/examples/modf.c (pull from upstream if you have to)
2) Write get_int_part and run the tests.
Write a function called get_int_part that takes as parameters an array of doubles and the integer number of elements in the array. It should allocate an array of doubles the same size as the input, and then use modf to compute the integer part of each element in the array, storing the result in the new array, and then returning the new array.
For example, if the inputs are the array {2.718, 3.142} and the integer 2, the output should be the new array {2.0, 3.0}.
Note: you must use modf even though you could do the same thing with a type cast.
3) Write get_both_parts and run the tests.
TOS talks about the main uses of bitwise operators (clearing, setting, and flipping bits), but there are also some other operations that may be useful when manipulating bits. When you use clever tricks like this in your code, be sure to comment them well so you understand them the next time you read it!
Some bit manipulation operations show up frequently enough that CPU designers have implemented special instructions to handle them. Since these are optional extensions, they won't be supported by all processors and your compiler may not generate them.
Thinking about bit-level manipulations often goes hand in hand with thinking about cycle-level performance. In this case, it's helpful to "think like the machine" to the extent possible. For example:
e = a / b * c / d;
is doing significantly more work than
s = r << 2;
q = s & t | u;
x = s + q;
even though it is written in fewer lines and does fewer operations.
Rules of thumb:
bitwise operations are fast (single cycle)
addition takes longer due to carries, but is so important that it will generally be a single cycle on most architectures
integer subtraction is the same as addition thanks to 2's complement (how?)
multiplication takes longer than addition, and may actually be multiple cycles depending on architecture. Your friendly compiler may replace some multiplys with a sequence of shifts and adds.
division takes longer than multiplication
floating point operations generally take longer than their integer counterparts, but floating point multiplication is faster than floating point addition (why?)
Zooming out a bit, the time it takes to access memory may dominate all of those computation times. It's helpful for you as a programmer to have a sense of the order of magnitude latencies we're dealing with for different types of memory accesses. Processors are pretty fast these days; the real challenge is keeping them fed with data.
Functional decomposition is the foundation of software engineering, and unit testing is the most important tool for developing functions.
The core idea of unit testing is that every function should have a corresponding set of tests, where each test calls the function with a particular set of parameters, and checks that the result is correct.
A complete test suite should also include tests with deliberate errors, and check that the function produces the correct error-handling behavior (as specified by the API).
Unit testing
1) Increases the chances that the implementation of the function is correct (although it is not sufficient).
2) Tends to drive designs toward better-designed APIs.
3) Facilitates refactoring, which is an essential tool for maintaining and improving code quality.
Most programming languages provide frameworks and tools to make unit testing easier.
There are many unit testing frameworks for C, at various levels of features/complexity.
The one we'll see today is MinUnit, which is, as the name implies, pretty much the minimal possible unit testing framework.
Exercise: Let's start the unit testing part of Exercise 4 now.
Some software engineers recommend test-driven development (TDD).
A defining element of TDD is writing the tests first.
Benefits of TDD include:
1) The tests serve as a detailed spec before you write the implementation, and as a detailed form of documentation afterward.
2) Writing the tests first forces you to design the API first, which is a good thing when it is possible.
3) TDD lends itself to a style of incremental development that prioritizes the most important features.
4) Realistically, if you don't write the tests first, you probably never will.
Unit tests also lend themselves to an effective work flow for handling bug reports:
1) If you discover a bug after deployment, that means you failed to write a test that would have detected the bug. So the first step is to write that test. It should fail.
2) Fix the bug. The new test should now pass. And all the old tests should pass, too.